Cells

Cells

Out of the frustration with Rails’ view layer, its lack of encapsulation and the convoluted code resulting from partials and helpers both accessing global state, the Cells gem emerged.

The cells gem is completely stand-alone and can be used without Trailblazer.

A cell is an object that represent a fragment of your UI. The scope of that fragment is up to you: it can embrace an entire page, a single comment container in a thread or just an avatar image link.

In other words: A cell is an object that can render a template.

Cells are faster than ActionView. While exposing a better performance, you step-wise encapsulate fragments into cell widgets and enforce interfaces.

View Model

Think of cells, or view models, as small Rails controllers, but without any HTTP coupling. Cells embrace all presentation and rendering logic to present a fragment of the UI.

Rendering a Cell

Cells can be rendered anywhere in your application. Mostly, you want to use them in controller views or actions to replace a complex helper/partial mess.

  - # app/views/comments/index.html.haml
  %h1 Comments
  @comments.each do |comment|
    = concept("comment/cell", comment) #=> Comment::Cell.new(comment).show

This will instantiate and invoke the Comment::Cell for each comment. It is completely up to the cell how to return the necessary markup.

Cell Class

Following the Trailblazer convention, the Comment::Cell sits in app/concepts/comment/cell.rb.

  class Comment::Cell < Cell::ViewModel
    def show
      "This: #{model.inspect}"
    end
  end

Per default, the #show method of a cell is called when it is invoked from a view. This method is responsible to compile the HTML (or whatever else you want to present) that is returned and displayed.

Whatever you pass into the cell via the concept helper will be available as the cell’s #model.
Whatever you return from the show method will be displayed in the page invoking the cell.

  = concept("comment/cell", comment) #=> "This: <Comment body=\"MVC!\">"

Note that you can pass anything into a cell. This can be an ActiveRecord model, a PORO or an array of attachments. The cell provides access to it via model and it’s your job do use it correctly.

Cell Views

While we already have a cleaner interface as compared to helpers accessing to global state, the real power of Cells comes when rendering views. This, again, is similar to controllers.

  class Comment::Cell < Cell::ViewModel
    def show
      render # renders app/concepts/comment/views/show.haml
    end
  end

Using #render without any arguments will parse and interpolate the app/concepts/comment/views/show.haml template. Note that you’re free to use ERB, Haml, or Slim.

  - # app/concepts/comment/views/show.haml
  %li
    = model.body
    By
    = link_to model.author.email, author_path(model.author)

That’s right, you can use Rails helpers in cell views.

No Helpers

While you could reference model throughout your view and strongly couple view and model, Cells makes it extremely simple to have logicless views and move presentation code to the cell instance itself.

  - # app/concepts/comment/views/show.haml
  %li
    = body
    By #{author_link}

Every method invoked in the view is called on the cell instance. This means we have to implement #body and #author_link in the cell class. Note that how that completely replaces helpers with clean object-oriented methods.

  class Comment::Cell < Cell::ViewModel
    def show
      render
    end

  private
    def body
      model.body
    end

    def author_link
      link_to model.author.email, author_path(model.author)
    end
  end

What were global helper functions are now instance methods. All Rails helpers like link_to are available on the cell instance.

Properties

Delegating attributes to the model is so common it is built into Cells.

  class Comment::Cell < Cell::ViewModel
    property :body
    property :author

    def show
      render
    end

  private
    def author_link
      link_to author.email, author_path(author)
    end
  end

The ::property declaration will create a delegating method for you.

Testing

The best part about Cells is: you can test them in isolation. If they work in a test, they will work just anywhere.

  describe Comment::Cell do
    it do
      html = concept("comment/cell", Comment.find(1)).()
      expect(html).to have_css("h1")
    end
  end

The concept helper will behave exactly like in a controller or view and allows you to write rock-solid test for view components with a very simple API.

More

Cells comes with a bunch of helpful features like nesting, caching, view inheritance, and more.