Is it just the crude title or is it true…? What makes Helpers Are Shit my most popular post ever? I felt like I have to write a follow-up now that Cells view models are replacing more and more helpers in many production apps out there.
Right, the last post is from 2011, that is old. What’s bad about this is that the write-up is still 100% accurate about the mess helpers and partials create in Rails apps. Nothing has changed in the Rails view layers for many years.
Helpers Are Shit.
The way helpers are being used in Rails encourages users to write “script-like” code using global variables and procedural functions in an environment that calls itself “object-oriented”.
Whenever I am refactoring complex Rails apps, the view layer is the worst part with hundreds of redundant helper methods accessing instance variables, calling other helpers, passing around models and options, and rendering random partials from a global directory.
Again, this mess is a result from the lack of a view abstraction layer in Rails: Helper invocations and partial rendering all happens in one monolithic ActionView instance – this is failure by design.
Cells “view models” allow you to replace helpers entirely and forever. Instead of a global pile of mud you encapsulate parts of your view into a class. A class that can render templates, can inherit or mixin other methods, allows view inheritance, if needed.
This is faster than what you had before: In a view model, no data is copied between controller and view – the view model is the view context. Let me demonstrate that.
I will use the term “view model” and “cell” interchangeably (yepp, I had to look up that word).
Encapsulation Via Classes.
Encapsulating view logic happens in cell classes.
class CommentCell < Cell::ViewModel def show render end end
Think of a cell as you think of a Rails controller (without all the HTTP overhead, of course). Per design, a cell has one public method
#show. This is a convention. You can expose as many methods as you like from that cell. More on that later.
Where Is The Model?
I’ll walk you through that now. First, we need to render that cell somewhere in our app.
.sidebar = cell(:comment, Comment.last).show
Suppose this is somewhere in an application layout (it could be any view or in a controller). This will instantiate a
CommentCell and execute the show method. This is a bit like calling a helper.
Note that I pass in a
Comment model instance as the 2nd argument. This is the cell’s “model”, making the former a genuine view model™.
Let’s have a look again at the cell’s show method.
class CommentCell < Cell::ViewModel def show render end end
show method is called, it hits
render. That will simply invoke the cell’s rendering as known from a Rails controller. With one exception: the view is located in a separate directory. Here’s how a cell directory looks.
app ├── cells │ ├── comment_cell.rb │ ├── comment │ │ ├── show.haml
This is the old layout soon to be superceded by the Trailblazer-style “concept layout”. That doesn’t matter right now.
Good Old Views.
Now, let me show you how that
show.haml could look like.
%h1 Comment = model.body By = link_to model.author.name, model.author
- The cell’s model (the comment itself) is accessible via the
- You can still use helpers in a view. In this example, I use
No Code In My View!
We’ve learned that logic in views is bad. And this is exactly where view models start to be fun to work with.
class CommentCell < Cell::ViewModel def show render end private def body model.body end end
Remember: as the view model is the view context itself, any method called in the view will be simply called on the view model. That reduces complexity in the view a bit.
%h1 Comment = body By = link_to model.author.name, model.author
Still hating the link line. Why not move that into the cell, too?
class CommentCell < Cell::ViewModel def show render end private def body model.body end def author_link link_to model.author.name, model.author end end
Now, what? We can call helpers in the view model? Seerriously?
Well, I never said that helpers are a bad thing. They’re just shit when used to “encapsulate”. I personally love
form_for (sometimes). They save me a lot of work. I just don’t like the fact they all play together in a big monolithic class.
link_to can’t mess up anything as it is running in a limited scope – the cell instance.
The second refactoring reduces our view to a dumb template, as it should be.
%h1 Comment = body By = author_link
I love it already. What’s next?
The task of delegating methods to the model is so tedious that it is built into cells. We can get rid of the
class CommentCell < Cell::ViewModel property :body property :author def show render end private def author_link link_to author.name, author end end
propertys automatically delegates to the model, making it a no-brainer. It also states your dependencies to the model in a very clear way. Hooray to explicit code!
What if we had to customise the
body method, e.g. apply a markdown filter on the content?
class CommentCell < Cell::ViewModel include MarkdownHelper property :body # ... private def body markdown super end end
First, I include the magic markdown helper to import the
markdown method. Secondly, I can override the
body method and call
super to access the original property. Object-orientation back in the view land!
A cell is not limited to one public method. It’s good to have only one but sometimes it comes in handy to not having to introduce a new class for every “helper”.
class CommentCell < Cell::ViewModel # ... def show render end def related render end private # ...
Here, I add a second public method
related. This will render another view, the
related.haml. Dump it into the cell’s directory and you’re good to go.
app ├── cells │ ├── comment_cell.rb │ ├── comment │ │ ├── show.haml │ │ ├── related.haml
You can now use this “helper” anywhere you want.
= cell(:comment, Comment.last).related
Isn’t that nice? We encapsulated a huge part of a mess into a clean, reusable component. This feels good.
Don’t Stop Me Now!
I want to talk more about the benefits of cells, like view inheritance, testability, reusing components, hooking them directly to routes, sharing decorators between cells and other layers, but.. I’m writing this blog post at work and feel like I should stop now.
And, did you know? Recent surveys have concluded that 79.3% of all cells users did not regret their choice and can’t do without cells anymore.
Let me know if you have any questions.