LIKE US FOR UPDATES + GET A FREE STICKER PACK!

Cells

Cells API

Last updated 05 May 2017 cells v4.1

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:

gem "cells-erb"

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.