The Cells gem has been around in the Rails community for more than four years now. It has hundreds of pleased users who reported all kinds of use cases and how cells improved their apps. Here’s a compilation of the best practices.
The challenge
A shopping cart in a Rails app.
Usually you’d take a shared partial to display the cart in every controller.
%h1 Your cart - if items.blank? = render :partial => "shared/cart/blank" - else %ul - for i in items %li #{i}
Some view would render the cart somewhere.
'shared/cart/cart', :locals => {:items => current_items} %>
1. Keep your views dumb
The render :partial
looks harmless, and many Railers will argue “Well, you can put that in a helper.” – anyway, this little piece of code alreay contains a lot of knowledge. Too much.
That’s knowing
- which items to display (
current_items
) - the exact location of the cart partial
A dumb view should not know anything about file locations at all.
current_items %>
The CartCell
we’re rendering hides any physical file location from us.
2. Don’t put logic into views
Yeah, a snippet like
- if items.blank? = render :partial => "shared/cart/blank" - else ..
already contains decider logic – didn’t we learn something different from MVC?
class CartCell :blank end
The cell is supposed to render its display
view. If no items were passed, it renders the blank
view.
Any decider logic happens in a class, and not in some object-unoriented helper or partial.
3. Interfaces can save lives
So I used :locals
in my example above. However, I often see controllers where people just set instance variables and use them throughout all partials.
def index @items = current_items ..
The partial blindly accesses instance variables.
%ul - for i in @items
Every controller using that partial has to know which instance variables it has to set before rendering. It’s only a matter of time until some controller renders a partial without providing the queried ivar – and your code crashes.
Cells by contrast force you to define which variables you wanna pass – making you writing interfaces without noticing it.
current_items %>
4. Controllers should be slim
So instead of globally cluttering your controller with instance variables, put that into well-encapsulated cells.
class CartCell < Cell::Base def display @user = session[:user] render end
Each cell has a limited scope not polluting anything except itself.
5. Avoid helpers
Helpers are plain modules. And can be pain.
What if the item list in the cart would be sortable? People implement that with helpers.
module CartHelper def render_cart_sorted order = params[:order] || :asc items = @items.sort(order) render :partial => 'shared/cart/cart', :locals => {:items => items} end
This leads me to questioning myself “What is a helper? It’s not a view, but renders. It’s not a controller, but it aggregates data. What are helpers?”.
The answer: Helpers are shit. They completely break anything we learned from MVC and they are untestable.
class CartCell < Cell::Base def display setup! render end def setup! order = decide_order(params[:order]) @items = @opts[:items].sort(order) end def decide_order(order) order || :asc end end
Here, sorting (or delegating sorting to the model) is the controller’s job. It’s duty is to process user gestures (user clicks on sorting icon) and to aggregate data (actually sorts data for presentation).
This is called V/C glue code as it lives between view and controller.
Under no circumstances should this be done in a helper.
6. Unit-test your glue code
Brrr, a shiver runs up and down my spine. I just imagined how you’d test this helper code.
With cells, you’d first assure that your sorting is doin’ right.
class CartCellTest < Cell::TestCase test "sorting should be doin' right" do assert_equal :asc, cell(:cart).decide_order(nil) assert_equal :desc, cell(:cart).decide_order(:desc) end end
Easy. What’s next?
7. Functional-test your rendering
We usually write functional tests when it comes to rendering markup. With helpers and partials this can be really painful.
Cells are made for heavy-duty testing.
class CartCellTest "some bullshit"} invoke :display assert_select "ul li", 3 .. end end
Cells let you simply test what you just did – in a separate, well-encapsulated environment.
8. Get OOP back in your views
Unfortunately, the current Rails view layer discourages using modern concepts, like inheritance, encapsulation and OOP at all. Why?
Why should we have great models and sucky views?
Now imagine it’s getting christmas, and your cart should look christmessy. Like some snowflakes in the background and a smiling reindeer on the order button.
class XmasCartCell 'xmas' end end
We simply inherit
- the setup methods
- the behaviour
- the views
It’s just OOP, back in your views. No magic, no implicit syntax, just OOP from the book.
9. Teams love components
Mando works on the shopping cart, whereas Charles plays around with the bestseller cell. Both are working on features shown in the same controller.
However, Charles and Mando will never get into trouble with each other – both have a separate component which does not know anything about the outer world.
Mando is strictly bound to
app/cells/ cart_cell.rb cart/ display.haml order_button.haml
Charles codes somewhere else in
app/cells/ bestseller_cell.rb bestseller/ list_latest.haml
They have separate tests, too. And when they think they’re stable they just plug the cells in the controllers and it will work.
Mando and Charles love components!
10. Hide your caching
The BestsellerCell
needs a lot of computation. Who bought what, when and how often? Why not cache the view?
According to the Rails docs, partials do that with the following pattern.
Our bestsellers: "bestseller", :collection => Bestseller.find(:all) %>
Caching is exposed to the action. The action has to know about the partial’s caching requirements. This is wrong.
The partial computing and rendering the list knows best about its needs. So put the caching where it belongs – to the partial!
class BestsellerCell 10.minutes def list @bestseller = Bestseller.find(:all) render end end
When calling
render_cell :bestseller, :list
nobody except the cell itself knows about the caching for that special page fragment. Even the expiry strategy is in the cell. This is called knowledge hiding.
More to come
The component-oriented approach with cells yields more improvements for your code. And there is more, yet. Interactive cells with AJAX often needed for rich client applications or dashboards – we got a cells-backed framework for that: Apotomo.
Excellent job with Cells Nick. I am again looking at it and I am starting to get more and more convinced that all of the Rails projects I’ve worked on should be using this mechanism.
LikeLike
Cells seem quite cool.
Does it work well with rails 3?
I’m about to write an app where the users home page is made up of blocks which they can move around the page (twitter feed, blog, flickr feed,…). Cells seem ideal.
LikeLike
good points 🙂
LikeLike
Wouldn’t it be better if you could just use normal controllers as cells? Then you wouldn’t have to worry that in future you might need to make a cell out of a piece of functionality.
Surely this would be easily possible with rails 3 new architecture?
LikeLike
@Chris – Cells in Rails 3 are derived from AbstractController, so you’re on the safe side.
Concerning your dashboard, check out Apotomo if your widgets should be interactive – if they are render-only, use Cells.
LikeLike
It’s true! Mando and Charles DO love components :).
LikeLike
Really helpful, Thank You 🙂
LikeLike
High 5 Nick, Cells saved me out of the box from all this pain in the viewss you mentioned. Using them happily in 2 Rails3 projects, integrating seamlessly with other gems like devise. Any core contribution plans?
LikeLike
I especially like the part about the cell taking care of fetching and caching its own data. It’ll definitely be in the next stuff I’m planning to do (probably with Apotomo too!). Great job 🙂
LikeLike
great writeup! good explanation and many reasons to finally use cells 🙂
LikeLike
Well made arguments. Makes me feel like Cells is what many think of as the presenter pattern. We will be using Cells on our next project do to your write up Nick, thanks!
LikeLike