While hacking on Cells 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, 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

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.

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.

Use case: Sidebar widgets

Usually we need shared partials whenever we want a reusable view component.

A sidebar box showing links to unread posts.

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.

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.

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 < Cell::Rails
  def box
    @user_greeting  = user_greeting
    @post_links     = links
 
    render :layout => :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.

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}

A sidebar box showing links to unread posts.
And we get a list of links pointing to posts which the logged in user hasn’t read, yet. Yeah.

Override a helper, inherit a view!

The admin box shows links for editing posts.

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.

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.

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 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!

Tags: ,

13 Responses to “Pragmatic Rails: Thoughts on Views, Inheritance, View Inheritance and Rails 3.0.4”

  1. wow, view inheritance sounds a lot cool, do you know if this is going to chain also with views from ApplicationController ?

  2. awesome =D

    good approach

  3. This just creates sparks in my brain, thanks again for a great article :)


  4. mtin79

    allways some great insights here. thanks!


  5. nick

    @Sandro: Both ActionControllers and Cells have view inheritance. Is that what you’re asking for???

  6. This is very cool, and a welcome development.

  7. 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?


  8. nick

    @Rami: Your example still suffers from a DoubleRenderError which is why Cells were invented 4 years ago.

    So, Cells are derived AbstractControllers 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!

  9. 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.

  10. 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?

    class GenericCell < Cell::Rails
      build do |opts|
          return AdminCell if current_user.admin?
          UserCell
      end
    end
     
    class AdminCell < UserCell
     
      def links
        current_user.unread_posts.collect do |p|
          [p.title, post_path(p)]
        end
      end
    end
     
    class UserCell < Cell::Rails
      def box
        @user_greeting  = user_greeting
        @post_links     = links
     
        render :layout => :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

    EDIT (nick): fixed that ;-)

    Sorry if there is no formatting in the code.

    Cheers


  11. nick

    @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:

    class AdminCell < UserCell
      supercell.build do
        # set things up here
      end

    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” ;-)


  12. Kevin Triplett

    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.


  13. Maxim Filatov

Leave a Reply