Cells

View Components for Rails.

Overview

Say you're writing a Rails online shop - the shopping cart is reappearing again and again in every view. You're thinking about a clean solution for that part. A mixture of controller code, before-filters, partials and helpers?

No. That sucks. Take Cells.

Cells are View Components for Rails. They look and feel like controllers. They don't have no DoubleRenderError. They can be rendered everywhere in your controllers or views. They are cacheable, testable, fast and wonderful. They bring back OOP to your view and improve your software design.

And the best: You can have as many cells in your page as you need!

Note: Since version 3.9 cells comes with two "dialects": You can still use a cell like a controller. However, the new view model "dialect" allows you to treat a cell more object-oriented while providing an alternative approach to helpers.

Installation

It's a gem!

Rails >= 3.0:

gem install cells

Rails 2.3:

gem install cells -v 3.3.9

Generate

Creating a cell is nothing more than

rails generate cell cart show -e haml
create  app/cells/
create  app/cells/cart
create  app/cells/cart_cell.rb
create  app/cells/cart/show.html.haml
create  test/cells/cart_test.rb

That looks very familiar.

Render the cell

Now, render your cart. Why not put it in layouts/application.html.erb for now?

<div id="header">
  <%= render_cell :cart, :show, :user => @current_user %>

Feels like rendering a controller action. For good encapsulation we pass the current user from outside into the cell - a dependency injection.

Code

Time to improve our cell code. Let's start with app/cells/cart_cell.rb:

class CartCell < Cell::Rails
  def show(args)
    user    = args[:user]
    @items  = user.items_in_cart

    render  # renders show.html.haml
  end
end

Is that a controller? Hell, yeah. We even got a #render method as we know it from the good ol' ActionController.

Views

Since a plain call to #render will start rendering app/cells/cart/show.html.haml we should put some meaningful markup there.

#cart
  You have #{@items.size} items in your shopping cart.

ERB? Haml? Builder?

Yes, Cells support all template types that are supported by Rails itself. Remember- it's a controller!

Helpers

Yes, Cells have helpers just like controllers. If you need some specific helper, do

class CartCell < Cell::Rails
  helper MyExtraHelper

and it will be around in your cart views.

Partials?

Yeah, we do support rendering partials in views. Nevertheless, we discourage partials at all.

The distinction between partials and views is making things more complex, so why should we have two kinds of view types? Use ordinary views instead, they're fine.

%p
  = render :view => 'items'

Rendering Global Partials

Sometimes you need to render a global partial from app/views within a cell. For instance, the gmaps4rails helper depends on a global partial. While this breaks encapsulation it's still possible in cells - just add the global view path.

class MapCell < Cell::Rails
  append_view_path "app/views"

View Inheritance

This is where OOP comes back to your view.

  • Inherit code into your cells by deriving more abstract cells.
  • Inherit views from parent cells.

Builders

Let render_cell take care of creating the right cell. Just configure your super-cell properly.

class LoginCell < Cell::Rails
  build do
    UnauthorizedUserCell unless logged_in?
  end

A call to

render_cell(:login, :box)

will render the configured UnauthorizedUserCell instead of the original LoginCell if the login test fails.

Caching

Cells allow you to cache per state. It's simple: the rendered result of a state method is cached and expired as you configure it.

To cache forever, don't configure anything

class CartCell < Cell::Rails
  cache :show

  def show
    render
  end

This will run #show only once, after that the rendered view comes from the cache.

Cache Options

Note that you can pass arbitrary options through to your cache store. Symbols are evaluated as instance methods, callable objects (e.g. lambdas) are evaluated in the cell instance context allowing you to call instance methods and access instance variables. All arguments passed to your state (e.g. via render_cell) are propagated to the block.

cache :show, :expires_in => 10.minutes

If you need dynamic options evaluated at render-time, use a lambda.

cache :show, :tags => lambda { |*args| tags }

If you don't like blocks, use instance methods instead.

class CartCell < Cell::Rails
  cache :show, :tags => :cache_tags

  def cache_tags(*args)
    # do your magic..
  end

Conditional Caching

The +:if+ option lets you define a condition. If it doesn't return a true value, caching for that state is skipped.

cache :show, :if => lambda { |*| has_changed? }

Cache Keys

You can expand the state's cache key by appending a versioner block to the ::cache call. This way you can expire state caches yourself.

class CartCell < Cell::Rails
  cache :show do |options|
    options[:items].md5
  end

The block's return value is appended to the state key: "cells/cart/show/0ecb1360644ce665a4ef".

Inheritance

Cache configuration is inherited to derived cells.

A Note On Fragment Caching

Fragment caching is not implemented in Cells per design - Cells tries to move caching to the class layer enforcing an object-oriented design rather than cluttering your views with caching blocks.

If you need to cache a part of your view, implement that as another cell state.

Testing Caching

If you want to test it in development, you need to put config.action_controller.perform_caching = true in development.rb to see the effect.

Testing

Another big advantage compared to monolithic controller/helper/partial piles is the ability to test your cells isolated.

Test::Unit

So what if you wanna test the cart cell? Use the generated test/cells/cart_cell_test.rb test.

class CartCellTest < Cell::TestCase
  test "show" do
    invoke :show, :user => @user_fixture
    assert_select "#cart", "You have 3 items in your shopping cart."
  end

Don't forget to put require 'cell/test_case' in your project's test/test_helper.rb file.

Then, run your tests with

rake test:cells

That's easy, clean and strongly improves your component-driven software quality. How'd you do that with partials?

RSpec

If you prefer RSpec examples, use the rspec-cells gem for specing.

it "should render the posts count" do
  render_cell(:posts, :count).should have_selector("p", :content => "4 posts!")
end

To run your specs we got a rake task, too!

rake spec:cells

View Models

Cells 3.9 brings a new dialect to cells: view models.

Think of a view model as a cell decorating a model or a collection. In this mode, helpers are nothing more than instance methods of your cell class, making helpers predictable and scoped.

class SongCell < Cell::Rails
  include Cell::Rails::ViewModel

  property :title


  def show
    render
  end

  def self_link
    link_to(title, song_url(model))
  end
end

Creation

Creating the view model should usually happen in the controller.

class DashboardController < ApplicationController

  def index
    @song = Song.find(1)

    @cell = cell(:song, @song)
  end

You can now grab an instance of your cell using the #cell method. The 2nd argument will be the cell's decorated model.

Have a look at how to use this cell in your controller view.

= @cell.show # renders its show view.

You no longer use the #render_cell helper but call any method on that cell. Usually, this is a state (or "action") like show.

Helpers

Note that this doesn't have to be a rendering state, it could be any instance method (aka "helper").

= @cell.self_link

As all helpers are now instance methods, the #self_link example can use any existing helper (as the URL helpers) on the instance level.

Attributes declared using `::property are automatically delegated to the decorated model.

@cell.title # delegated to @song.title

Views

This greatly reduces wiring in the cell view (which is still in app/cells/song/show.haml).

%h1
  = title

Bookmark! #{self_link}

Making the cell instance itself the view context should be an interesting alternative for many views.

Mountable Cells

Cells 3.8 got rid of the ActionController dependency. This essentially means you can mount Cells to routes or use them like a Rack middleware. All you need to do is derive from Cell::Base.

class PostCell < Cell::Base
  ...
end

In your routes.rb file, mount the cell like a Rack app.

match "/posts" => proc { |env|
  [ 200, {}, [ Cell::Base.render_cell_for(:post, :show) ]]
}

Cells in ActionMailer

ActionMailer doesn't have request object, so if you inherit from Cell::Rails you will receive an error. Cell::Base will fix that problem, but you will not be able to use any of routes inside your cells.

You can fix that with actionmailer_with_request which (suprise!) brings request object to the ActionMailer.

Using Rails Gems Like simple_form Outside Of Rails

Cells can be used outside of Rails. A new module brought in 3.8.5 provides the Rails view "API" making it possible to use gems like the popular simple_form outside Rails!

All you need to do is providing the cell with some helpers, usually it's the polymorphic routing paths required by the gems.

module RoutingHelpers
  def musician_path(model)
    "/musicians/#{model.id}"
  end
end

Then, use the Cell::Rails::HelperAPI module and it should work fine (depending on the quality of the gem you're desiring to use).

require 'cell/base'
require "cell/rails/helper_api"
require "simple_form"

class BassistCell < Cell::Base
  include Cell::Rails::HelperAPI

  self._helpers = RoutingHelpers

  def show
    @musician = Musician.find(:first)
  end
end

Your views can now use the gem's helpers.

<%= simple_form_for @musician do |f| %>
  <%= f.input :name %>
  <%= f.button :submit %>
<% end %>

Note that this currently "only" works with Rails 3.2-4.0.

Cells is Rails::Engine aware!

Now Rails::Engines can contribute to Cells view paths. By default, any 'app/cells' found inside any Engine is automatically included into Cells view paths. If you need to, you can customize the view paths changing/appending to the 'app/cell_views' path configuration. See the Cell::EngineIntegration for more details.

Generator Options

By default, generated cells inherit from Cell::Rails. If you want to change this, specify your new class name in config/application.rb:

Base Class

module MyApp
  class Application < Rails::Application
    config.generators do |g|
      g.base_cell_class "ApplicationCell"
    end
  end
end

Base Path

You can configure the cells path in case your cells don't reside in app/cells.

config.generators do |g|
  g.base_cell_path "app/widgets"
end

Rails 2.3 note

In order to copy the cells rake tasks to your app, run

script/generate cells_install

Capture Support

If you need a global #content_for use the cells-capture gem.

More features

Cells can do more.

  • No Limits. Have as many cells in your page as you need - no limitation to your render_cell calls.
  • Cell Nesting. Have complex cell hierarchies as you can call render_cell within cells, too.

Go for it, you'll love it!

LICENSE

Copyright (c) 2007-2013, Nick Sutterer

Copyright (c) 2007-2008, Solide ICT by Peter Bex and Bob Leers

Released under the MIT License.