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.
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.
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.
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.
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.
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.
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.
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.
Cells comes with a bunch of helpful features like nesting, caching, view inheritance, and more.
The following happens now.
Comment::Cell::Show
cell is rendered.Pro::Cell::Layout
is invoked and it will yield the content from the show cell.layout.haml
view, the navigation cell is also rendered. This cell needs a current_user
option which is provided by the 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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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)
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.
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] }
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.
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
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"
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
.
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.
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 "haml", github: "haml/haml", ref: "7c7c169".
# Use `cells-hamlit` instead.
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
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 #=> <script>Dangerous</script>
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) %>
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 dup
ed when adding to it into nested cells. This is to prevent leaking nested state back into parent objects.
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.
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"]
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.
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.
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
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? }
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"
.
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 configuration is inherited to derived cells.
If you want to test it in development
, you need to update development.rb
to see the effect.
config.action_controller.perform_caching = true
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”.
gem "trailblazer-cells"
gem "cells-slim"
Make sure you also add the view engine. We recommend cells-slim
.
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
.
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
).
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.
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.
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"]).()
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.
Only a few methods are needed to integrate cells testing into your test suite. This is implemented in Cell::Testing
.
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
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
.
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 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
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
.
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
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.
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
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.
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"
.
When using cells in a Rails app there’s several nice features to benefit from.
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 constantize
d 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
*/
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
*/
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
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.
In Rails, you can generate cells and concept cells.
rails generate cell comment
Or, TRB-style concept cells.
rails generate concept comment
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.
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
You might have to include cells’ template gem into your application’s Gemfile
. This will properly require the extension.
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"
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
.
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.
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.
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.
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.
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
Sometimes you have to manually include Rails helpers into your cell classes. For instance, the following exception originating from lib/action_view/helpers/form_options_helper.rb
might disturb your sunny day.
undefined method `collection_select' for #<...::Cell::
While we still don’t understand the root of the problem, it can be fixed by including FormOptionsHelper
.
module Comment::Cell
class New < Trailblazer::Cell
include ActionView::Helpers::FormOptionsHelper
# ...
end
end
Likewise, when using simple_form
you need to manually include its helper or you will encounter the following exception.
undefined method `simple_form_for' for #<...::Cell::
It is easily fixed with an include
.
module Transaction::Cell
class New < Trailblazer::Cell
include SimpleForm::ActionViewExtensions::FormHelper
# you can include more than one helper:
# include ActionView::Helpers::FormOptionsHelper