Cells

  • Last updated 25 Jun 20

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.

Overview

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.

Getting Started

The Cells gem provides view models for Ruby web applications. View models are plain objects that represent a part of the web page, such as a dashboard widget. View models can also render views, and be nested.

Cells is a replacement for ActionView and used in many Rails projects. However, Cells can be used in any web framework such as Sinatra or Hanami.

This guide discusses how to get started with Trailblazer::Cell, the canonical view model implementation following the Trailblazer file and naming structure. Don’t worry, Trailblazer::Cell can be used without Trailblazer.

Refactoring Legacy Views

When refactoring legacy views into a solid cell architecture, it is often advisable to start with small fragments and extract markup and logic into an object-oriented cell. After that is done, you can move up and replace a bigger fragment of the view, and so on.

Given you were running an arbitrary Ruby web application, let’s assume you had a menu bar sitting in your global layout. The menu shows navigation links to pages and - dependent on the login status of the current user - either a miniature avatar of the latter or a link to sign in.

Since this is quite a bit of logic, it’s a good idea to encapsulate that into an object.

Here’s what the old legacy layout.html.haml looks like.

  %html
    %head

    %body
      %nav.top-bar
        %ul
          %li SUPPORT CHAT
          %li GUIDES
          %li
            - if signed_in?
              %img{src: avatar_url}
            - else
              "SIGN IN"
<html>
  <head></head>

  <body>
    <nav class="top-bar">
      <ul>
        <li>SUPPORT CHAT</li>
        <li>GUIDES</li>
        <li>
          <% if signed_in? %>
            <img src="<%= avatar_url %>">
          <% else %>
            SIGN IN
          <% end %>
        </li>
      </ul>
    </nav>
  </body>
</html>

Of course, this navigation bar doesn’t really make sense without any links added. I’ve left that out so we can focus on the structure. We will discuss how helpers work in the per-framework sections below.

Extraction

In order to convert all markup below the <nav> node into a cell, we first need to add the respective gems to our Gemfile.

For now, all you need is the trailblazer-cells gem and the Cells template engine.

gem "trailblazer-cells"
gem "cells-hamlit"

In Rails, you need the trailblazer-cells gem and the Cells template engine.

gem "trailblazer-cells"
gem "cells-hamlit"
gem "cells-rails"

The cells-rails gem will add cell invocation helpers to controllers and views, and supply the URL helpers in cells.

Please note that trailblazer-cells simply loads the cells gem and then adds some simple semantics on top of it. Also, Cells supports Haml, Hamlit, ERB, and Slim.

Cut the %nav fragment from the original layout.html.haml and replace it with the cell invocation.

<ul id=".//2.1/snippets/gems/cells/getting_started.md.erb-91" class="nav nav-tabs"> <li class="nav-item"> <a href="#" class="nav-link pink active active" data-toggle="tab" data-tag="#-2-1-snippets-gems-cells-getting_started-md-erb-91-HAML"> HAML </a> </li><li class="nav-item"> <a href="#" class="nav-link pink " data-toggle="tab" data-tag="#-2-1-snippets-gems-cells-getting_started-md-erb-91-ERB"> ERB </a> </li> </ul><div class="tab-content"> <div id="-2-1-snippets-gems-cells-getting_started-md-erb-91-HAML" class="tab-pane fade show active"> 
      %html
        %head

        %body
          = Pro::Cell::Navigation.(nil, current_user: current_user).()

</div><div id="-2-1-snippets-gems-cells-getting_started-md-erb-91-ERB" class="tab-pane fade show ">

      <html>
        <head></head>

        <body>
          <%= Pro::Cell::Navigation.(nil, current_user: current_user).() %>
        </body>
      </html>

</div> </div>

Cells can be invoked using the call style.

<ul id=".//2.1/snippets/gems/cells/getting_started.md.erb-115" class="nav nav-tabs"> <li class="nav-item"> <a href="#" class="nav-link pink active active" data-toggle="tab" data-tag="#-2-1-snippets-gems-cells-getting_started-md-erb-115-HAML"> HAML </a> </li><li class="nav-item"> <a href="#" class="nav-link pink " data-toggle="tab" data-tag="#-2-1-snippets-gems-cells-getting_started-md-erb-115-ERB"> ERB </a> </li> </ul><div class="tab-content"> <div id="-2-1-snippets-gems-cells-getting_started-md-erb-115-HAML" class="tab-pane fade show active"> 
      %html
        %head

        %body
          = cell(Pro::Cell::Navigation, nil, current_user: current_user)

</div><div id="-2-1-snippets-gems-cells-getting_started-md-erb-115-ERB" class="tab-pane fade show ">

      <html>
        <head></head>

        <body>
          <%= cell(Pro::Cell::Navigation, nil, current_user: current_user) %>
        </body>
      </html>

</div> </div>

In Rails, you can use the handy `cell` helper to invoke the view model.

Instead of keeping navigation view code in the layout, or rendering a partial, the Pro::Cell::Navigation cell is now responsible to provide the HTML fragment representing the menu bar.

The cell is invoked without a model, but we pass in the current_user as an option. This code assumes that the current_user object is available in the layout view. As the current user is passed from your web framework to the cell, this is called an dependency injection.

We will learn what models and options are soon.

Navigation Cell

Having extracted the “partial” from the layout, paste it into a new file app/concepts/pro/view/navigation.haml.

%nav.top-bar
  %ul
    %li SUPPORT CHAT
    %li GUIDES
    %li
      - if signed_in?
        %img{src: avatar_url}
      - else
        "SIGN IN"

Creating a view for the cell in the correct directory is one thing. A cell is more than just a partial, it also needs a class file where the logic sits. This class goes to app/concepts/pro/cell/navigation.rb.

module Pro
  module Cell
    class Navigation < Trailblazer::Cell
      include ::Cell::Hamlit

      def signed_in?
        options[:current_user]
      end

      def email
        options[:current_user].email
      end

      def avatar_url
         hexed = Digest::MD5.hexdigest(email)
        "https://www.gravatar.com/avatar/#{hexed}?s=36"
      end
    end
  end
end

The reason the cell class lives in the Pro namespace is because that’s the example app’s name and its top-level namespace. Since the navigation cell is an app-wide concept, it is best put into the Pro namespace.

Adding the class is enough to re-render your application, and you will see, the navigation menu now comes from a cell, presenting you with a hip gravatar icon when logged in, or a string to do so otherwise. Congratulations.

Discussion: Navigation

Let’s quickly discuss what happens here in what order. After this section, you will understand how cells work and probably already plan where else to use them. They’re really simple!

  1. Invoking the cell in the layout via Pro::Cell::Navigation.(nil, current_user: current_user).() will instantiate the cell object and internally invoke the cell’s default rendering method, named show. This method is automatically provided and simply renders the corresponding view.
  2. Since the cell class name is Pro::Cell::Navigation, this cell will render the view concepts/pro/view/navigation.haml. This is following the Trailblazer naming style.
  3. In the cell’s view, two “helpers” are called: signed_in? and avatar_url. Whatsoever, the concept of a “helper” in Cells doesn’t exist anymore. Any method or variable called in the view must be an instance method on the cell itself. This is why the cell class defines those two methods, and not some arbitrary helper module.
  4. Dependencies like the current_user have to get injected from the outer world when invoking the cell. Later, in the cell, those arguments can be accessed using the options cell method.

It is important to understand that the cell has no access to global state. You as the cell author have to define the interface and the dependencies necessary to render the cell.

It is a good idea to write tests for you cell now, to document and assert this very interface you’ve just created.

Test: Navigation

Testing cells can either happen via full-stack integration tests, or with module cell unit tests. This example illustrates the latter.

A very basic test for a cell with signed in user could looks as follows.

class NavigationCellTest < Minitest::Spec
  it "renders avatar when user provided" do
    html = Pro::Cell::Navigation.(nil, current_user: User.find(1)).()

    html.must_match "Signed in: nick@trb.to"
  end
end

Likewise, you can easily test the anonymous user case, where no one’s logged in. The unit-test style makes it very easy to simulate that.

it "renders SIGN IN otherwise" do
  html = Pro::Cell::Navigation.(nil, current_user: nil).()

  html.must_match "SIGN IN"
end

Make sure to read the full documentation on testing cells for all kinds of environments, including Capybara matchers, Rails and test frameworks such as Minitest and Rspec.

URL helpers

Besides indicating the logged-in status of the use, the Navigation cell should also display real links. This happens with URL helpers in web frameworks.

In app/concepts/pro/view/navigation.haml, add actual links to the view.

  %nav
    %ul
      %li SUPPORT CHAT
        %a{href: "/support"}
      %li GUIDES
        %a{href: "/guides"}
      %li
        - if signed_in?
          %img{src: avatar_url}
        - else
          "SIGN IN"

Without any URL system, you can create links using Haml, or delegate to your own URL objects.

  %nav
    %ul
      %li
        = link_to "SUPPORT CHAT", chat_path
      %li
        = link_to "GUIDES", guides_path
      %li
        - if signed_in?
          %img{src: avatar_url}
        - else
          "SIGN IN"

The cells-rails gem conveniently allows to use URL helpers in cell views.

Content Cell

View models are not only great to encapsulate small fragments, but also the entire content - which is everything but the wrapping layout. For example, when browsing to /comments/1, the content view usually provided by a controller will now come from the Comment::Cell::Show cell. We call this a content cell in Trailblazer-speak.

In the controller, invoke this new cell instead of letting the framework render the content.

  def show
    comment = Comment.find(1)
    html = Comment::Cell::Show.(comment).()
  end
  class CommentsController < ApplicationController
    def show
      comment = Comment.find(1)
      render html: cell(Comment::Cell::Show, comment)
    end
  end

This instructs the controller to render the Comment::Cell::Show cell, pass the comment into it, and let Rails wrap it in the application layout.

Note that we now pass the comment as the cell’s model without passing any additional options.

The new class lives in app/concepts/comment/cell/show.rb.

module Comment::Cell
  class Show < Trailblazer::Cell
    property :body
  end
end

The property method creates a shortcut to model.body.

The view for this cell goes into app/concepts/comment/view/show.haml.

%h1 Comment #{model.id}
%p
  = body

In views, you can access the model via the model method. In this example, this is the comment instance we passed into the cell when invoking.

Layout Cell

To render the entire page with Cells, the last missing piece is the layout. This is still being rendered by the underlying framework.

Again, the layout is represented by a cell. Its code goes to app/concepts/pro/cell/layout.rb.

module Pro::Cell
  class Layout < Trailblazer::Cell
  end
end

Since this cell’s sole purpose is to render a view to wrap dynamic content, no logic is needed.

The view sits at app/concepts/pro/view/layout.haml.

%html
  %head

  %body
    = cell(Pro::Cell::Navigation)
    = yield

The content cell’s HTML markup will be passed to the layout cell and can be yielded (which basically replaces yield with the HTML string).

Putting Things Together

In the controller, the content cell needs to get instructed it will be wrapped by a layout cell.

  def show
    comment = Comment.find(1)

    html = Comment::Cell::Show.(comment,
      context: { current_user: current_user },
      layout:  Pro::Cell::Layout).()
  end
  class CommentsController < ApplicationController
    def show
      comment = Comment.find(params[:id])

      render html: cell(Comment::Cell::Show, comment,
        context: { current_user: current_user },
        layout:  Pro::Cell::Layout
        )
    end
  end

The following happens now.

  1. First, Comment::Cell::Show cell is rendered.
  2. Then, Pro::Cell::Layout is invoked and it will yield the content from the show cell.
  3. In the layout.haml view, the navigation cell is also rendered. This cell needs a current_user option which is provided by the context object.

Context Object

When rendering a hierarchy of cells, it’s often necessary to share generic data across all involved cells. This always happens via dependency injection, no global state is allowed in Trailblazer.

The :context option will create such an object and is automatically being passed to all cells in that render workflow.

In order to access the current_user from the context object, the navigation cell has to be changed slightly.

module Pro
  module Cell
    class Navigation < Trailblazer::Cell
      include ::Cell::Hamlit

      def signed_in?
        context[:current_user]
      end

      def email
        context[:current_user].email
      end

      def avatar_url
         hexed = Digest::MD5.hexdigest(email)
        "https://www.gravatar.com/avatar/#{hexed}?s=36"
      end
    end
  end
end

Note that :current_user is not an option anymore, but comes from the context object.

Success

As demonstrated in this guide, it’s not really hard to replace the existing rendering stack, whether that’s ActionView or Hanami::View or Sinatra templates, with Cells.

API

A cell is an object that can render views. It represents a fragment of the page, or the entire page.

Sometimes they’re also called object-oriented partials.

The object has to define at least one method which in turn has to return the fragment. Per convention, this is #show. In this public method, you may compile arbitrary strings or render a cell view.

The return value of this public method (also called state) is what will be the rendered in the view using the cell.

Anatomy

Cells usually inherit from Cell::ViewModel.

class CommentCell < Cell::ViewModel
  def show
    render # renders app/cells/comment/show.haml
  end
end

When the CommentCell cell is invoked, its show method is called, the view is rendered, and returned as a HTML string.

This snippet illustrates a suffix cell, because it follows the outdated Rails-style naming and file structure. We encourage you to use Trailblazer cells. However, this document mostly describes the generic API.

Show

As per convention, #show is the only public method of a cell class.

The return value of this method is what gets rendered as the cell.

def show
  "I don't like templates!"
end

You’re free to return whatever string you desire, use your own rendering engine, or use cells’ render for templates.

Manual Invocation

In its purest form, a cell can be rendered as follows.

Comment::Cell.new(comment).() #=> "I don't like templates!"

This can be split up into two steps: initialization and invocation.

Initialize

You may instantiate cells manually, wherever you want.

cell = Comment::Cell.new(comment)

This is helpful in environments where the helpers are not available, e.g. a Rails mailer or a Lotus::Action.

Note that usually you pass in an arbitrary object into the cell, the “model”. Here, this is the comment instance.

Model

The model you pass into the cell’s constructor is completely up to you! It could be an ActiveRecord instance, a Struct, or an array of items.

The model is available via the model reader.

def show
  model.rude? ? "Offensive content." : render
end

The term model is really not to be confused with the way Rails uses it - it can be just anything.

Property

Cells allow a short form to access model’s attributes using the property class method.

class CommentCell < Cell::ViewModel
  property :email #=> model.email

  def show
    model.email #=> "s@trb.to"
    email #=> "s@trb.to"
  end
end

Using ::property will create a convenient reader method for you to the model.

Options

Along with the model, you may also pass arbitrary options into the cell, for example the current user.

cell = Comment::Cell.new(comment, current_user: current_user)

In the cell, you can access any options using the options reader.

def show
  options[:current_user] ? render : "Not logged in!"
end

Invocation

Once you’ve got the cell instance, you may call the rendering state. This happens via ViewModel#call.

cell.call(:show)

It’s a common idiom in Ruby to skip the explicit call method name. The next snippet does the same as the above.

cell.(:show)

Since show is the default state, you may simple call the cell without arguments.

cell.() #=> cell.(:show)

Note that in Rails controller views, this will be called automatically via cell’s ViewModel#to_s method.

Call

Always invoke cell methods via call. This will ensure that caching - if configured - is performed properly.

CommentCell.new(comment).(:show)

As discussed, this will call the cell’s show method and return the rendered fragment.

Note that you can invoke more than one state on a cell, if desired.

- cell = CommentCell.new(Comment.last)      # instantiate.
= cell.call(:show)                          # render main fragment.
= content_for :footer, cell.(:footer)       # render footer.

See how you can combine cells with global helpers like content_for?

You can also provide additional arguments to call.

cell.(:show, Time.now)

All arguments after the method name are passed to the invoked method.

def show(time)
  time #=> Now!
end

Even blocks are allowed.

cell.(:show) { "Yay!" }

Again, the block is passed through to the invoked method.

def show(&block)
  yield #=> "Yay!"
end

This is particularly interesting when passing the block to render and using yield in the view. See render’s docs for that.

Default Show

Per default, every cell derived from Cell::ViewModel has a built-in show method.

class CommentCell < Cell::ViewModel
  # #show is inherited.
end

The implementation looks as follows.

def show(&block)
  render &block
end

An optional block is always passed to the render method.

Of course, you’re free to override show to do whatever it needs to do.

Instantiation Helper

In most environments you will instantiate cells with the concept or cell helper which internally does exactly the same as the manual invocation.

cell = cell(:comment, comment)

This is identical to

cell = CommentCell.new(comment)

Depending on your environment, the cell helper might inject dependencies into the created cell. For example, in Rails, the controller is passed on into the cell behind the scenes. When manually instantiating cells, you must not forget to do so, too.

The cell helper also allows passing in the cell constant. This means, it won’t try to infer the class constant name.

cell = cell(CommentCell, comment)

File Structure

Having a cell to represent a fragment of your page is one thing. The real power, whatsoever, comes when rendering templates in cells. The render method does just that.

In a suffix environment, Cells expects the following file layout.

├── app
│   ├── cells
│   │   └── comment_cell.rb
│   │   └── comment
│   │       └── show.haml

Every cell - unless configured otherwise - has its own view directory named after the cell’s name (comment). Views do only have one extension to identify the template’s format (show.haml). Again, you’re free to provide arbitrary additional extensions.

Note that the suffix style shown above is deprecated, and will be superseded in Cells 5 by the Trailblazer-style naming and file structure.

Render

class CommentCell < Cell::ViewModel
  def show
    render # renders show.haml.
  end
end

A simple render will implicitly figure out the method (or state) name and attempt to render that view. Here, the file will be resolved to app/cells/comment/show.haml.

Note that render literally renders the template and returns the HTML string. This allows you to call render multiple times, concatenate, and so on.

def show
  render + render(:footer) + "<hr/>"
end

You can provide an explicit view name as the first argument.

def show
  render :item # renders item.haml
end

When providing more than one argument to render, you have to use the :view option.

def show
  render view: :item # renders item.haml
end

If you like the clunky Rails-style file naming, you’re free to add a format to the view.

render "shot.html" # renders show.html.haml

You can pass locals to the view using :locals.

render locals: { logged_in: options[:current_user] }

Instance Methods

While it is fine to use locals or instance variables in the view to access data, the preferred way is invoking instance methods in the view.

%h1 Show comment

= body
= author_link

Every method call in the view is dispatched to the cell instance. You have to define your “helpers” there.

class CommentCell < Cell::ViewModel
  property :body # creates #body reader.

  def author_link
    url_for model.author.name, model.author
  end
end

This allows slim, logic-less views.

Note that you can use Rails in the instance level, too, if you’re in a Rails environment.

Yield

A block passed to the cell constructor is passed on to the state method.

CommentCell.new(comment) { "Yay!" }
cell(:comment, comment)  { "Yay!" }

It’s up to you if you want to use this block, or provide your own.

def show(&block)
  render(&block)
end

Passing the block render allows yielding it in the view.

%h1 Comment

= yield

Collection

Instead of manually iterating an array of models and concatenating the output of the item cell, you can use the :collection feature.

cell(:comment, collection: Comment.all).()

This will instantiate a cell per iterated model, invoke call and join the output into one fragment.

Pass the method name to call when you want to invoke a state different to show.

cell(:comment, collection: Comment.all).(:item)

You’re free to pass additional options to the call.

cell(:comment, collection: comments, size: comments.size).()

This instantiates each collection cell as follows.

CommentCell.new(comment, size: 9)

You can use the join method to customize how each item in the collection is invoked. The return value of the block is automatically inserted in between each rendered item in the collection0

class CommentCell < Cell::ViewModel
  def odd
    "odd comment\n"
  end

  def even
    "even comment\n"
  end
end

cell(:comment, collection: Comment.all).join do |cell, i|
  i.odd? ? cell.(:odd) : cell(:even)
end
# => "odd comment\neven comment\nodd comment\neven comment"

An optional separator string can be passed to join when it concatenates the item fragments.

cell(:comment, collection: Comment.all).join("<hr/>") do |cell, i|
  i.odd? ? cell.(:odd) : cell(:even)
end
# => "odd comment\n<hr/>even comment\n<hr/>odd comment\n<hr/>even comment"

Alternatively, if you just want to add some extra content in between each rendered item and don’t need to customize how each item is invokved, you can call join with a separator and no block:

class PostCell
  def show
    'My post'
  end
end

cell(:post, collection: Post.all).join("<hr/>")
# => "My post<hr/>My post<hr/>My post"

External Layout

Since Cells 4.1, you can instruct your cell to use a second cell as a wrapper. This will first render your actual content cell, then pass the content via a block to the layout cell.

Cells desiring to be wrapped in a layout have to include Layout::External.

class CommentCell < Cell::ViewModel
  include Layout::External
end

The layout cell usually can be an empty subclass.

class LayoutCell < Cell::ViewModel
end

Its show view must contain a yield to insert the content.

!!!
%html
  %head
    %title= "Gemgem"
    = stylesheet_link_tag 'application', media: 'all'
    = javascript_include_tag 'application'
  %body
    = yield

The layout cell class is then injected into the actual invocation using :layout.

cell(:comment, comment, layout: LayoutCell)

The context object will automatically be passed to the layout cell.

Note that :layout also works in combination with :collection.

View Paths

Per default, the cell’s view path is set to app/cells. You can set any number of view paths for the template file lookup.

class CommentCell < Cell::ViewModel
  self.view_paths = ["app/views"]

Note that the default view paths are different if you’re using the Trailblazer-style file structure.

Template Formats

Cells provides support for a handful of popular template formats like ERB, Haml, etc.

You need to add the specific template engine to your Gemfile:

In Rails, this is all you need to do. In other environments, you need to include the respective module into your cells.

class CommentCell < Cell::ViewModel
  include ::Cell::Erb # or Cell::Hamlit, or Cell::Haml, or Cell::Slim
end

HTML Escaping

Cells per default does no HTML escaping, anywhere. This is one of the reasons why Cells is much faster than Rails’ ActionView.

Include Escaped to make property readers return escaped strings.

class CommentCell < Cell::ViewModel
  include Escaped
  property :title
end

song.title                 #=> "<script>Dangerous</script>"
Comment::Cell.(song).title #=> &lt;script&gt;Dangerous&lt;/script&gt;

Only strings will be escaped via the property reader.

You can suppress escaping manually.

def raw_title
  "#{title(escape: false)} on the edge!"
end

Of course this works in views too:

<%= title(escape: false) %>

Context Object

By default, every cell contains a context object. When nesting cells, this object gets passed in automatically. To add objects to the context, use the :context option.

cell("comment", comment, context: { user: current_user })

You can read from the context object via the context method.

def show
  context[:user] #=> <User ..>
  # ..
end

The context object is handy when dependencies need to be passed down (or up, when using layouts) a cell hierarchy.

Note that the context object gets duped when adding to it into nested cells. This is to prevent leaking nested state back into parent objects.

Nesting

You can invoke cells in cells. This happens with the cell helper.

def show
  html = cell(:comment_detail, model)
  # ..
end

The cell helper will automatically pass the context object to the nested cell.

View Inheritance

Cells can inherit code from each other through Ruby’s regular inheritance mechanisms.

class CommentCell < Cell::ViewModel
end

class PostCell < CommentCell
end

Even cooler, PostCell will now inherit views from CommentCell.

PostCell.prefixes #=> ["app/cells/post", "app/cells/comment"]

When views can’t be found in the local post directory, they will be looked up in comment. This starts to become helpful when using composed cells.

If you only want to inherit views, not the entire class, use ::inherit_views.

class PostCell < Cell::ViewModel
  inherit_views Comment::Cell
end

PostCell.prefixes #=> ["app/cells/post", "app/cells/comment"]

Builder

Often, it’s good practice to replace decider code from views or classes by extracting it out into separate sub-cells. Or in case you want to render a polymorphic collection, builders come in handy.

Builders allow instantiating different cell classes for different models and options.

class CommentCell < Cell::ViewModel
  include ::Cell::Builder

  builds do |model, options|
    if model.is_a?(Post)
      PostCell
    elsif model.is_a?(Comment)
      CommentCell
    end
  end
end

The #cell helper takes care of instantiating the right cell class for you.

cell(:comment, Post.find(1)) #=> creates a PostCell. This also works with collections.

cell(:comment, collection: [@post, @comment]) #=> renders PostCell, then CommentCell.

Multiple calls to ::builds will be ORed. If no block returns a class, the original class will be used (CommentCell). Builders are inherited.

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
    order.id
  end

The versioner block is executed in the cell instance context, allowing you to access all stakeholder objects you need to compute a cache key. The return value is appended to the state key: "cells/cart/show/1".

As everywhere in Rails, you can also return an array.

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

Resulting in: "cells/cart/show/1/0ecb1360644ce665a4ef".

Debugging Cache

When caching is turned on, you might wanna see notifications. Just like a controller, Cells gives you the following notifications.

  • write_fragment.action_controller for cache miss.
  • read_fragment.action_controller for cache hits.

To activate notifications, include the Notifications module in your cell.

class Comment::Cell < Cell::Rails
  include Cell::Caching::Notifications

Cache Inheritance

Cache configuration is inherited to derived cells.

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.

Trailblazer

This documents the Trailblazer-style cells semantics, brought to you by the trailblazer-cells gem.

This gem can be used stand-alone without Trailblazer, its only dependency is the cells gem.

A Trailblazer::Cell is a normal cell with Trailblazer semantics added. Naming, file structure, and the way views are resolved follow the TRB style. Note that this will be the standard for Cells 5, which will drop all old “dialects”.

Installation

gem “trailblazer-cells” gem “cells-slim”

Make sure you also add the view engine. We recommend cells-slim.

File Structure

In Trailblazer, cell classes sit in their concept’s cell directory, the corresponding views sit in the view directory.

├── app │   ├── concepts │   │   └── comment # namespace/class │   │   ├── cell # namespace/module │   │   │   ├── index.rb # class │   │   │   ├── new.rb # class │   │   │   └── show.rb # class │   │   └── view │   │   ├── index.slim │   │   ├── item.slim │   │   ├── new.slim │   │   ├── show.slim │   │   └── user.scss

Note that one cell class can have multiple views, as well as other assets like .scss stylesheets.

Also, the view names with Trailblazer::Cell are not called show.slim, but named after its corresponding cell class. For instance, Comment::Cell::Index will render comment/view/index.slim.

Naming

As always, the Trailblazer naming applies.

Comment[::SubConcepts]::Cell::[Name]

This results in classes such as follows.

module Comment::Cell # namespace class New < Trailblazer::Cell # class def show render # renders app/concepts/comment/view/new.slim. end end end

This is different to old suffix-cells. While the show method still is the public method, calling render will use the new.slim view, as inferred from the cell’s last class constant segment (New).

Default Show

Note that you don’t have to provide a show method, it is created automatically.

module Comment::Cell class New < Trailblazer::Cell end end

This is the recommended way since no setup code should be necessary.

You’re free to override show, though.

View Names

Per default, the view name is computed from the cell’s class name.

Comment::Cell::New #=> “comment/view/new.slim” Comment::Cell::Themed::New #=> “comment/view/themed/new.slim”

Note that the entire path after Cell:: is considered, resulting in a hierarchical view name.

Use ViewName::Flat if you prefer a flat view name.

module Comment module Cell module Themed class New < Trailblazer::Cell extend ViewName::Flat end end end end

Comment::Cell::Themed::New #=> “comment/view/new.slim”

This will always result in a flat name where the view name is inferred from the last segment of the cell constant.

Invocation

To render a cell in controllers, views, or other cells, use cell. You need to provide the constant directly. Ruby’s constant lookup rules apply.

html = cell(Comment::Cell::New, result["model"]).()

Layouts

It’s a common pattern to maintain a cell representing the application’s layout(s). Usually, it resides in a concept named after the application.

├── app │   ├── concepts │   │   └── gemgem │   │   ├── cell │   │   │   ├── layout.rb │   │   └── view │   │   ├── layout.slim

Most times, the layout cell can be an empty subclass.

module Gemgem::Cell class Layout < Trailblazer::Cell end end

The view gemgem/view/layout.slim contains a yield where the actual content goes.

!!! %html %head %title= “Gemgem” = stylesheet_link_tag ‘application’, media: ‘all’ = javascript_include_tag ‘application’ %body = yield

Wrapping the content cell (Comment::Cell::New) with the layout cell (Gemgem::Cell::Layout) happens via the public :layout option.

concept(“comment/cell/new”, result[“model”], layout: Gemgem::Cell::Layout)

This will render the Comment::Cell::New, instantiate Gemgem::Cell::Layout and pass through the context object, then render the layout around it.

Testing

Only a few methods are needed to integrate cells testing into your test suite. This is implemented in Cell::Testing.

API

Regardless of your test environment (Rspec, MiniTest, etc.) the following methods are available.

module Testing
  concept(name, *args) # instantiates Cell::Concept subclass.
  cell(name, *args) # instantiates Cell::ViewModel subclass.
end

Calling the two helpers does exactly the same it does in a controller or a view.

Usually, this will give you the cell instance. It’s your job to invoke a state using #call.

it "renders cell" do
  cell(:song, @song).() #=> HTML / Capybara::Node::Simple
end

However, when invoked with :collection, it will render the cell collection for you. In that case, #cell/#concept will return a string of markup.

it "renders collection" do
  cell(:song, collection: [@song, @song]) #=> HTML
end

MiniTest, Test::Unit

In case you’re not using Rspec, derive your tests from Cell::TestCase.

class SongCellTest < Cell::TestCase
  it "renders" do
    cell(:song, @song).().must_have_selector "b"
  end
end

You can also include Cell::Testing into an arbitrary test class if you’re not happy with Cell::TestCase.

Optional Controller

If your cells have a controller dependency, you can set it using ::controller.

class SongCellTest < Cell::TestCase
  controller SongsController

This will provide a testable controller via #controller, which is automatically used in Testing#concept and Testing#cell.

Rspec

Rspec works with the rspec-cells gem.

Make sure to install it.

gem “rspec-cells”

You can use the #cell and #concept builders in your specs.

describe SongCell, type: :cell do
  subject { cell(:song, Song.new).(:show) }

  it { expect(subject).to have_content "Song#show" }
end

Optional Controller

If your cells have a controller dependency, you can set it using ::controller.

describe SongCell do
  controller SongsController

This will provide a testable controller via #controller.

Capybara Support

Per default, Capybara support is enabled in Cell::TestCase when the Capybara gem is loaded. This works for both Minitest and Rspec.

The only changed behavior will be that the result of Cell#call is wrapped into a Capybara::Node::Simple instance, which allows to call matchers on the result.

cell(:song, @song).().must_have_selector "b" # example for MiniTest::Spec.

In case you need access to the actual markup string, use #to_s. Note that this is a Cells-specific extension.

cell(:song, @song).().to_s.must_match "by SNFU" # example for MiniTest::Spec.

You can disable Capybara for Cells even when the gem is loaded.

Cell::Testing.capybara = false

Capybara with Minitest (Rails)

With Minitest, the recommended approach is to use the minitest-rails-capybara gem.

group :test do
  gem "minitest-rails-capybara"
end

You also have to include certain Capybara modules into your test case. It’s a good idea to do this in your app-wide test_helper.rb.

Cell::TestCase.class_eval do include ::Capybara::DSL include ::Capybara::Assertions end

If you miss to do so, you will get an exception similar to the following.

NoMethodError: undefined method `must_have_css’ for #<User::Cell::Index:0xb5a6c>

Here’s an example how we do it in Gemgem.

Capybara with Minitest::Spec

In a non-Rails environment, the capybara_minitest_spec gem is what we use.

group :test do
  gem "capybara_minitest_spec"
end

Add the following to your test_helper.rb.

require "capybara_minitest_spec"
Cell::Testing.capybara = true

After including the Testing module, you’re ready to run specs against cells.

class NavigationCellTest < Minitest::Spec
  include Cell::Testing

  it "renders avatar when user provided" do
    html = cell(Pro::Cell::Navigation, user).()

    html.must_have_css "#avatar-signed-in"
    html.to_s.must_match "Signed in: nick@trb.to"
  end
end

Rendering

View Paths

Every cell class can have multiple view paths. However, I advise you not to have more than two, better one, unless you’re implementing a cell in an engine. This is simply to prevent unexpected behavior.

View paths are set via the ::view_paths method.

class Cell::ViewModel
  self.view_paths = ["app/cells"]

Use the setter to override the view paths entirely, or append as follows.

class Shopify::CartCell
  self.view_paths << "/var/shopify/app/cells"

The view_paths variable is an inheritable array.

A trick to quickly find out about the directory lookup list is to inspect the ::prefixes class method of your particular cell.

puts Shopify::CartCell.prefixes
#=> ["app/cells/shopify/cart", "/var/shopify/app/cells/shopify/cart"]

This is the authorative list when finding templates. It will include inherited cell’s directories as well when you used inheritance. The list is traversed from left to right.

Partials

Even considered a taboo, you may render global partials from Cells.

SongCell < Cell::ViewModel
  include Partial

  def show
    render partial: "../views/shared/sidebar.html"
  end

Make sure to use the :partial option and specify a path relative to the cell’s view path. Cells will automatically add the format and the terrible underscore, resulting in "../views/shared/_sidebar.html.erb".

Rails

When using cells in a Rails app there’s several nice features to benefit from.

Asset Pipeline

Cells can bundle their own assets in the cell’s view directory. This is a very popular way of writing highly reusable components.

It works with both engine cells and application cells.

├── cells
│   ├── comment_cell.rb
│   ├── comment
│   │   ├── show.haml
│   │   ├── comment.css
│   │   ├── comment.coffee

You need to register the cells with bundled assets. Preferably, this happens in config/application.rb of the main application.

class Application < Rails::Application
  # ..
  config.cells.with_assets = ["comment_cell"]

The names added to with_assets have to be the fully qualified, underscored cell name. They will get constantized to find the cell name at runtime.

If using namespaces, this might be something along config.cells.with_assets = ["my_engine/song/cell"].

In app/assets/application.js, you need to add the cell JavaScript assets manually.

//=# require comments

Likewise, you have to reference the cell’s CSS files in app/assets/application.css.

/*
 *= require comment
 */

Asset Pipeline With Trailblazer

With Trailblazer, cells follow a different naming structure.

├── concepts
│   │   └── comment
│   │       ├── cell
│   │       │   ├── index.rb
│   │       │   └── show.rb
│   │       └── view
│   │           ├── index.haml
│   │           ├── show.haml
│   │           └── comment.scss

The comment concept here will provide Comment::Cell::Index and Comment::Cell::Show. Both bundle their assets in the comment/view directory.

To add this to Rails’ asset pipeline, you need to reference one of the cell classes in config/application.rb.

class Application < Rails::Application
  # ..
  config.cells.with_assets = ["comment/cell/index"] # one of the two is ok.

You still need to require the JS and CSS files. Here’s an example for app/assets/application.css.

/*
 *= require comment # refers to concepts/comment/view/comment.scss
 */

Assets Troubleshooting

The Asset Pipeline is a complex system. If your assets are not compiled, start debugging in Cells’ railtie and uncomment the puts in the cells.update_asset_paths initializer to see what directories get added.

Cell classes need to be loaded when precompiling assets! Make sure your application.rb contains the following setting (per default, this is turned on).

config.assets.initialize_on_precompile = true

You need to compile assets using this command, which is explained here.

rake assets:precompile:all RAILS_ENV=development RAILS_GROUPS=assets

Global Partials

Although not recommended, you can also render global partials from a cell. Be warned, though, that they will be rendered using our stack, and you might have to include helpers into your view model.

This works by including Partial and the corresponding :partial option.

class Cell < Cell::ViewModel include Partial

def show
  render partial: "../views/shared/map.html" # app/views/shared/map.html.haml
end

The provided path is relative to your cell’s ::view_paths directory. The format has to be added to the file name, the template engine suffix will be used from the cell.

You can provide the format in the render call, too.

render partial: “../views/shared/map”, formats: [:html]

This was mainly added to provide compatibility with 3rd-party gems like Kaminari and Cells that rely on rendering partials within a cell.

Generators

In Rails, you can generate cells and concept cells.

rails generate cell comment

Or, TRB-style concept cells.

rails generate concept comment

Engine Cells

You can bundle cells into Rails engines and maximize a clean, component architecture by making your view models easily distributable and overridable.

This pretty much works out-of-the-box, you write cells and push them into an engine. The only thing differing is that engine cells have to set their view_paths manually to point to the gem directory.

Engine View Paths

Each engine cell has to set its view_paths.

The easiest way is to do this in a base cell in your engine.

module MyEngine
  class Cell < Cell::Concept
    view_paths = ["#{MyEngine::Engine.root}/app/concepts"]
  end
end

The view_paths is inherited, you only have to define it once when using inheritance within your engine.

module MyEngine
  class Song::Cell < Cell # inherits from MyEngine::Cell

This will not allow overriding views of this engine cell in app/cells as it is not part of the engine cell’s view_paths. When rendering MyEngine::User::Cell or a subclass, it will not look in app/cells.

To achieve just that, you may append the engine’s view path instead of overwriting it.

class MyEngine::User::Cell < Cell::Concept
  view_paths << "#{MyEngine::Engine.root}/app/concepts"
end

Engine Render problems

You might have to include cells’ template gem into your application’s Gemfile. This will properly require the extension.

Engine Namespace Helpers

If you need namespaced helpers, include the respective helper in your engine cell.

module MyEngine class CommentCell < Cell::ViewModel include Engine.routes.url_helpers

  def comment_url
    link_to model.title, engine_specific_path_without_any_namespaces_needed
  end
end   end


# application Gemfile
gem "cells-erb"

Translation and I18N Helper

You can use the #t helper.

require "cell/translation"

class Admin::Comment::Cell < Cell::Concept
  include ActionView::Helpers::TranslationHelper
  include ::Cell::Translation

  def show
    t(".greeting")
  end
end

This will lookup the I18N path admin.comment.greeting.

Setting a differing translation path works with ::translation_path.

class Admin::Comment::Cell < Cell::Concept
  include Cell::Translation
  self.translation_path = "cell.admin"

The lookup will now be cell.admin.greeting.

Asset Helper

When using asset path helpers like image_tag that render different paths in production, please simply delegate to the controller.

class Comment::Cell < Cell::Concept
  delegates :parent_controller, :image_tag

The delegation fixes the well-known problem of the cell rendering the “wrong” path when using Sprockets. Please note that this fix is necessary due to the way Rails includes helpers and accesses global data.

Template Engines

Cells supports various template engines.

We provide support for Haml, Erb, and Slim. You can also write your own template engine.

In a non-Rails environment, you need to include the respective module into your cells, so it knows what template to find.

class SongCell < Cell::ViewModel
  include Cell::Erb
  # include Cell::Haml
  # include Cell::Slim

Note that you can only include one engine per class. This is due to problems with helpers in Rails and the way they have to be fixed in combination with Cells.

Multiple Template Engines in Rails

When including more than one engine in your Gemfile in Rails, the last one wins. Since each gem includes itself into Cell::ViewModel, unfortunately there can only be one global engine.

Currently, there’s no clean way but to disable automatic inclusion from each gem (not yet implemented) and then include template modules into your application cells manually.

ERB

Haml

Slim

Your Own

Theoretically, you can use any template engine supported by Tilt.

To activate it in a cell, you only need to override #template_options_for.

class SongCell < Cell::ViewModel
  def template_options_for(options)
    {
      template_class: Tilt, # or Your::Template.
      suffix:         "your"
  }
  end

This will use Tilt to instantiate a template to be evaluated. The :suffix is needed for Cells when finding the view.

Troubleshooting

Helper Inclusion Order

One of the many problems with Rails helper implementation is that the inclusion order matters. This can lead to problems with the following exception.

undefined method `output_buffer=' for #<Comment::Cell:0xb518d8cc>

Usually, the problem arises when you have initializers to setup your application cell. When including helpers here, they might be included before the cells gem has a chance to include its fixes.

Please include your template engine module explicitly then, after your standard helper inclusions.

# config/initializers/trailblazer.rb

Cell::Concept.class_eval do
  include ActionView::Helpers::TranslationHelpers # include your helpers here.
  include Cell::Erb # or Cell::Slim, or Cell::Haml after that
end