{{{
While hacking on “Cells”:http://cells.rubyforge.org/ for Rails 3 with Yehuda earlier this year we were discussing if Cells’ view inheritance will be superseded by Rails.Yehuda patiently *postponed any work on it* with the words _”we will do that for you”_ 🙂 Apparently, he didn’t lie.
It seems that “view inheritance will be available directly in Rails 3.0.4”:https://rails.lighthouseapp.com/projects/8994/tickets/948-template-lookup-should-search-controller-inheritance-chain, which is a fantastic improvement for Rails.
In this post I’d like to discuss
* *What _is_ view inheritance* and how does it help?
* *What’s the problem* when view inheritance is tightly bound to controllers?
* *How shared components* with inheritance can help
h3. What is View Inheritance?
Say we have a @PostsController@ in a blog app. *It shows a list of posts.* Wow. We also have a derived @PrivatePostsController@, here’s the inheritance chain.
PrivatePostsController < PostsController < ...
Both controllers have an @#index@ method.
class PostsController < ApplicationController def index @posts = Posts.public_posts_for_all_of_yer end class PrivatePostsController < PostsController def index @posts = Posts.private_posts end
While the @PostsController@ *_does_ have* an @index@ view, its child @PrivatePostsController@ *doesn’t*.
|-- posts | |-- index.html.haml |-- private_posts |-- backend
Now, when the private controller wants to render its @index@ view, it first looks in its own views directory. *If it can’t find a suitable view, it just travels up the inheritance chain.* It finds the parent’s @index@ view, and uses this.
*Cool, this is view inheritance.*
h3. Inheritance and partials?
The newly introduced mechanism *also applies to partials*, which is pretty awesome.
If the @index@ view would use a partial to show a list of navigation links, *the @PrivatePostController@ could “overwrite” just this piece of the page* by dropping a separate partial in its view directory.
|-- posts | |-- index.html.haml | |-- _navigation.html.haml |-- private_posts | |-- _navigation.html.haml
*Overriding view pieces done simple.* I’m happy this finally found its way into Rails! Good job, artemave.
h3. Use case: Sidebar widgets
Usually we need *shared partials whenever we want a reusable view component.*
}}}
{{{
What about a status box in our app exposing helpful links to the reader? If an *admin user* is logged in, he (or her) will *get links to _edit_ new, unread posts* and his own drafts.
An *editor* might see *links to _read_ new posts* assigned to him. His job is just proof-reading.
This is a typical setup in almost every Rails app, *small boxes display different informations according to the authenticated user*.
Usually *this ends up in a helper that delegates to different partials*, where lots of code is put into that partials.
def render_status_box if current_user.admin? render :partial => "shared/admin_box" elsif current_user.editor? render :partial => "shared/editor_box" # and so on...
The problem here is, *you simply cannot use inheritance anywhere.* Neither is it possible to derive helpers (they are modules), nor could view inheritance be applied here.
h3. You can’t inherit shared partials
Why that? Well, *the shared partial and the rendering controller are not related in any way.* Nevertheless, the _controller’s_ ancestor chain is inspected when trying to find an “inheritable” shared partial, *which is simply the wrong place*.
In other words, the partial lookup will climb up the inheritance chain of the controller _using_ the partial, and that’s *definitly the wrong place to search*.
The bottom line: *When it comes to reuseable view components, Rails’ view inheritance is still insufficient*.
h3. Reuseability with inheritance
So, what do to now? The answer, as usual: *Divide and conquer.* Split your view into object-oriented components and get back the power of inheritance.
You’re still with me? *Cool.*
First, I create *a cell that provides the basic functionalities* – generating a user greeting and compiling a list of links for the user.
class UserCell :box end def user_greeting "#{current_user.name} (#{current_user.role})" end def links current_user.unread_posts.collect do |p| [p.title, post_path(p)] end end end
If you’re new to cells, you might read “this introducing post”:http://nicksda.apotomo.de/2010/11/lets-write-a-reusable-sidebar-component-in-rails-3/.
h3. How to get that in my app?
Now I embedd the cell in the application layout.
#sidebar = render_cell :user, :box
This basically calls the @#box@ _state_ of our @UserCell@, which
* *compiles the greeting message* in @#user_greeting@. Note how this *removes the need for pushing complex helpers* into the view.
* *prepares a list of post links* in @#links@. While we could do the setup in the view, too, I prefer *placing that kind of code to the controller*, since its job is aggregating data. It also *improves unit-testability* and makes it easy to inherit and override!
The @render@ statement finally parses the cell’s view.
|-- app | |-- cells | | |-- user | | | |-- box.html.haml
The box view could look like this.
Hey, #{@user_greeting}, how are you? Check that: %ul - @post_links.each do |p| = %li #{link_to *p}
And we get a *list of links pointing to posts* which the logged in user hasn’t read, yet. Yeah.
h3. Override a helper, inherit a view!
When an admin user is logged in, we want a slightly different list, *showing links to _edit_ posts*.
Let’s use inheritance to do just that!
class AdminCell < UserCell def links current_user.pending_posts.collect do |p| [p.title, edit_post_path(p)] end end end
This is all, we just overwrite the helper. *Everything else is inherited from the @UserCell@.* The methods, the states, the views.
h3. No deciders in your views!
Now, *your application layout shouldn’t end up with deciders* figuring out which cell to render.
#sidebar - if current_user.admin? = render_cell :admin, :box - elsif current_user.editor? = render_cell :user, :box
Cells provides you with *a builder to prevent your view from being cluttered*.
class UserCell < Cell::Rails build do |opts| AdminCell if current_user.admin? end
That’s all! Now, calls to @#render_cell(:user, …)@ will *query the builder and internally create the correct cell* for you.
h3. It’s more than a feature
I hope the examples showed how view inheritance can help *cleaning up your code* while providing an object-oriented *view layer with real inheritance*. This is nothing exotic – many “cells”:https://github.com/apotonick/cells users have reported to use the techniques discussed in this post successfully.
And now, lemme quote my friend Kevin from Austin: _Merry Christmas and all that stuff!_
}}}
wow, view inheritance sounds a lot cool, do you know if this is going to chain also with views from ApplicationController ?
LikeLike
awesome =D
good approach
LikeLike
This just creates sparks in my brain, thanks again for a great article 🙂
LikeLike
allways some great insights here. thanks!
LikeLike
@Sandro: Both ActionControllers and Cells have view inheritance. Is that what you’re asking for???
LikeLike
This is very cool, and a welcome development.
LikeLike
View inheritance and what described in the post I linked below can do exactly what cells does just with native rails, no plugins:
http://amberbit.com/blog/render-views-partials-outside-controllers-rails-3
What do you think, Nick?
LikeLike
@Rami: Your example still suffers from a
DoubleRenderError
which is why Cells were invented 4 years ago.So, Cells are derived
AbstractController
s as well and use “native” rails methods as described in the blog post. However, Cells streamline the process of having reuseable controllers throughout your app (as it should be in a real MVC framework) and provides additional features like clean caching and using #params in your cell, which is not possible in your example.Don’t get me wrong – Cells is nothing special and I really hope that cells becomes superseded by a clean and refactored rendering API in Rails in the near future!
LikeLike
I really didn’t like this in UserCell class:
build do |opts|
AdminCell if current_user.admin?
end
AdminCell inherits from UserCell. So, UserCell shouldn’t know about its derived classes, in this case AdminCell.
That OOP mess.
LikeLike
Hi Fernando,
You are right about being a godd thing trying to minimize coupling between parent and children types. So let’s try to refactor out in a way that the parent class does not hold a reference to its children?
EDIT (nick): fixed that 😉
Sorry if there is no formatting in the code.
Cheers
LikeLike
@Fernando: I absolutely agree. So Guilherme’s approach is a dedicated configuration cell, which is
GenericCell
here.Another trick would be putting the builder code in the actual sub-cell:
This would work if the derived cell is required explicitly, otherwise the builder isn’t executed until the sub-cell is loaded.
You could also put the builder code in a separate configuration module and require that in an initializer.
As you might see, there are plenty of ways to prevent “OOP mess” 😉
LikeLike
Great post, now I understand view inheritance (wasn’t clear to me at first). And hope you had that merry Christmas and all the other stuff!
@Rami: one thing Nick left out was the testability of Cells versus partials. Cells can be tested in isolation whereas it’s impractical to test partials.
LikeLike
Milestone changed from 3.0.4 to 3.1
https://rails.lighthouseapp.com/projects/8994/tickets/948-template-lookup-should-search-controller-inheritance-chain#ticket-948-48
LikeLike