The Cells gem has helped many developers to re-structure and re-think their view layer in Rails. It provides view models that embrace parts of your UI into self-contained widgets.
What was partials, filters, helpers and controller code is now moved into a separate class. View models are plain Ruby and use OOP features like inheritance while benefiting from encapsulation. The times of global view namespace and lack of interfaces in views are over.
class CommentCell < Cell::ViewModel def show render end private def author_link link_to model.author end end
Cells can render their own views which sit in a private directory.
In views, we try to gently enforce simplicity: When calling a method in the view, it is called on the cell instance. The view is always executed in cells context. There is no concept of “helpers” and data being copied between controller and view anymore.
<%= model.body %> Written by <%= author_link %>
Every method called in the view has to be defined on the cell. Every helper you intend to use has to be included into the class – remember: everything is an instance method.
What makes me really happy about Cells 4 are the following two lines of code that have substantially changed how Cells does its work. Those lines represent the end of a painful era for Cells: they completely decouple the gem from Rails.
module Cell class ViewModel # < AbstractController::Base
That’s right, Cells does no longer inherits anything from
AbstractController. With our own implementation for rendering templates, we don’t need this dependency anymore. In earlier version this was mainly done to import Rails’
#render and the rabbit hole of dependencies coming with this.
spec.add_dependency "uber", "~> 0.0.9" spec.add_dependency 'tilt', ">= 1.4", "< 3" # s.add_dependency "actionpack", ">= 3.0"
We also removed the dependency to
actionpack, and, in turn, to
actionview. ActionView is no longer used in Cells, except for helpers, which brings me to the next point.
Long live Rails!
Hey, hey, don’t you cry. Cells still supports Rails and works exactly as it did before in Rails apps. It still provides Rails’ (actually, not-existing) view “API” and allows you to use helpers, form builders,
simple_form_for and all the good guys.
The difference here is you have to
include those helpers into your cell class. This might end up in quite a number of includes, as the following snippet illustrates.
class CommentCell < Cell::ViewModel include ActionView::RecordIdentifier include ActionView::Helpers::FormHelper include SimpleForm::ActionViewExtensions::FormHelper
This is not Cells’ fault, though.
Helpers Are Shit.
What gets unrevealed now is how horribly helpers are implemented in Rails. Not only do they all exist as global methods in one namespace, also do they all depend on each other without including the respective modules.
Helpers in Rails simply assume that all the other 250 helper functions are available.
It is now your task to properly include required helper modules yourself. Maybe this will spark an impulse in Rails core to properly decouple helpers, and use more object-orientation and composition instead of the current global PHP functions.
Anyway, most helpers are reported (and tested) to be working in Cells.
Performance. You asked for it.
In the 4.0 release we got rid of many many lines of code. We also got rid of ActionView. Replacing this jurassic gem with our own 30 lines rendering code has sped up rendering about 25%.
Performance gains could also be achieved by only escaping defined properties of a cell. Where Rails literally escapes every string several times per request, which leads to a significant performance decrease, Cells does this once, and only where you want it.
I need to remark that not any performance-relevant work has been done in Cells 4, yet. Path execution improvements will make this even faster in future versions.
Render cells works virtually from anywhere. In controllers and views, Cells brings in a helper to make it straight-forward.
Although this sounds like a contradiction – “didn’t you just say helpers are shit? – in fact this acts as a single entry point to invoke cells.
<%= cell(:comment, comment).(:show) %>
The new call style allows to work with the cell instance before rendering. And: you can have as many rendering methods (“states”, as we call them) as you want per cell class.
The same API can be used in tests. Cells comes with UnitTest/MiniTest support out of the box, and Rspec can be pulled via the rspec-cells gem.
it "renders nicely" html = cell(:comment, comment).() expect(html).to have_content "Hello!" end
Isolated view rendering tests are inevitable when writing rock-solid components that are resuable across your application.
Upgrading from Cells 3
View models have been around in Cells 3, too, but not as fast. Anyway, if you’re upgrading, you might want to peek inside the upgrading guide. Let us know if you find anything missing in there.
One thing I need to mention: You don’t need to rewrite all your cells – you can still use instance variables and the old-style calling – it’s just not encouraged anymore.
Another point you shouldn’t miss is to include the respective template engine support into your projects. Please read the installation instructions to learn about cells-haml and friends.
Many users do that already. I hear that Cells and Roda, a framework I really want to check out, do a great job together.
Outside of Rails, the only thing that needs configuration is where to find the views.
class SongCell < Cell::ViewModel self.view_paths = "lib/views"
After defining the
view_paths, cells can be rendered anywhere in your application.
This will instantiate the cell and render the
show state. Examples for how to use advanced features like caching and view inheritance can be found in my cells-examples repository.
Mailers, Rake Tasks, Here Comes Cells!
Cells have been used in Rails for many things: In mailers, in rake tasks to compile views, directly hooked to routes to bypass ActionController, and so on.
This is even simpler now as there is no dependency to drag around anymore. You simply instantiate and render your view model. However, some helpers still insist on a controller instance to operate properly. For example, they might need the
Pass the controller into a cell for that. Being a special dependency in a Rails environment, this will delegate all known controller methods to the real controller.
SongCell.(song, controller: controller).(:show)
We’ve been using this “technique” in Cells for years without major problems cough.
Engines and the Asset Pipeline
Cells can be bundled into gems and Rails engines and allow you to distribute them as proper widgets to other applications.
Nothing really changes, you simply chuck them into your gems and they become renderable in the importing application. If you’re having problems, it’s all documented on the new (and still under construction) Trailblazer website.
Another great thing is: you can bundle assets right into your cell’s
views directory and include them in your asset pipeline.
├── cells │ ├── comment_cell.rb │ ├── comment │ │ ├── show.haml │ │ ├── comment.css │ │ ├── comment.coffee
Using the asset pipeline is documented here.
Cells can inherit views from their parents. If a view is not found in the local directory, it is looked up in the parent’s directories.
class PostCell < CommentCell end PostCell.prefixes #=> ["app/cells/post", "app/cells/comment"]
The new explicit version of
prefixes makes it really simple to understand where views come from.
And we have another awesome feature planned for upcoming Cells versions: Block inheritance. That’s right: Block inheritance. This means you can define overridable parts directly in your view, without the need to implement that in a separate file.
Make sure to read the documentation about view inheritance and check out the Trailblazer book which will explain this nifty topic in great detail.
Cells 4 is clean and fast. Go through the code base, and you will see how incredibly simple it is. There will be problems with certain helpers or gems, but I am confident we can fix them together.
From the very beginning I put a lot of effort into communication with the different template engines teams, for example the fine peeps from Haml. My dream is to have a unified interface for capturing and helpers, so markup languages don’t need to get patched by Rails, or patch Rails, or both.
The next big step is evaluating how much of ActionView we can strip and replace with the learnings from 10 years of Cells without changing Rails’ API, dear DHH. I am currently experimenting with Rafael França in a top secret mission, so don’t tell anyone about this.