Three reasons to love ActionController::Responder

Posted by José Valim August 31, 2009 @ 06:02 PM

A couple weeks ago, I wrote about the newly added ActionController::Responder which summarizes your application behavior for a specified format in just one place. For example, the default html behavior is written as:


class ActionController::Responder
  def to_html
    if get?
      render
    elsif has_errors?
      render :action => (post? ? :new : :edit)
    else
      redirect_to resource
    end
  end
end

Here are three examples of the flexibility this new layer can provide.

1) HTTP Cache

A simple, but quite powerful use for responder is apply HTTP cache to all your resources easily. For simplicity, let's cache just get requests that are not handling a collection:


module CachedResponder
  def to_html
    if get? && resource.respond_to?(:updated_at)
      return if controller.fresh_when(:last_modified => resource.updated_at.utc)
    end
    super
  end
end

2) I18n flash messages

Is a common practice that all controllers, when the create, update and destroy actions are handled with success, a flash message is shown to the user. We could easily remove the flash messages from our controllers and let them be handled by the responder with the help of the I18n framework. And it’s quite straightforward to accomplish:


module FlashResponder
  # If it's not a get request and the object has no errors, set the flash message
  # according to the current action. If the controller is users/pictures, the
  # flash message lookup for create is:
  #
  #   flash.users.pictures.create
  #   flash.actions.create
  #
  def to_html
    unless get? || has_errors?
      namespace = controller.controller_path.split('/')
      namespace << controller.action_name
      flash[:success] = I18n.t(namespace.join("."), :scope => :flash,
       :default => "actions.#{controller.action_name}", :resource => resource.class.human_name)
    end
    super
  end
end

The first question then arises: what if I don’t want to add a flash message in an specific situation? This can be solved using options, since all options sent to respond_with are sent to the responder, we could use it in our favor as well:


class MyResponder < ActionController::Responder
  def to_html
    unless get? || has_errors? || options.delete(:flash) == false
      namespace = controller.controller_path.split('/')
      namespace << controller.action_name
      flash[:success] = I18n.t(namespace.join("."), :scope => :flash,
       :default => "actions.#{controller.action_name}", :resource => resource.class.human_name)
    end
    super
  end
end

And we can invoke it as:


class PostsController < ApplicationController
  def create
    @post = Post.create(params[:post])
    respond_with(@post, :flash => false)
  end
end

3) Instant pagination

Some people already start a project with pagination from scratch, others add at some point. Nonetheless, pagination is more like a rule than a exception. Can that be handled by Rails 3? First, let’s check an index action with respond_with:


class PostsController < ApplicationController
  def index
    @posts = Post.all
    respond_with(@posts)
  end
end

Right now, when we call Post.all, it returns a collection of posts in an array, so the pagination should be done before the collection is retrieved. Thanks to Emilio and his work integrating ActiveRelation with ActiveRecord, Post.all will return an ActiveRecord::Relation that will be sent to the responder:


module PaginatedResponder
  # Receives a relation and sets the pagination scope in the collection
  # instance variable. For example, in PostsController it would
  # set the @posts variable with Post.all.paginate(params[:page]).
  def to_html
    if get? && resource.is_a?(ActiveRecord::Relation)
      paginated = resource.paginate(controller.params[:page])
      controller.instance_variable_set("@#{controller.controller_name}", paginated)
    end
    super
  end
end

However, the code above is definitely smelling. Set the paginated scope seems more to be a controller responsability. So we can rewrite as:


module PaginatedResponder
  def to_html
    if get? && resource.is_a?(ActiveRecord::Relation)
      controller.paginated_scope(resource)
    end
    super
  end
end

class ApplicationController < ActionController::Base
  def paginated_scope(relation)
    instance_variable_set "@#{controller_name}", relation.paginate(params[:page])
  end
  hide_action :paginated_scope
end

As previously, you could make use of some options to customize the default pagination behavior.

Wrapping up

All the examples above were contained in modules, that means that our actual responder has yet to be created:


class MyResponder < ActionController::Responder
  include CachedResponder
  include FlashResponder
  include PaginatedResponder
end

To activate it, we just need to overwrite the responder method in our application controller:


class ApplicationController < ActionController::Base
  def paginated_scope(relation)
    instance_variable_set "@#{controller_name}", relation.paginate(params[:page])
  end
  hide_action :paginated_scope

  protected
  def responder
    MyResponder
  end
end

While those examples are simple, they show how you can dry up your code easily using Responder. And you? Already thought in an interesting use case for it?

Posted in Edge | 9 comments

Comments

  1. Jonathan on 31 Aug 21:24:

    Readability completely sacrificed for “elegance.”

    One step forward, two steps back.

  2. iain on 31 Aug 21:39:

    @jonathan: sure, keep writing out the entire workflow of every action as much as you like.

    I like the new responder. I welcome any good effort in minimizing duplication in my code. Controllers were always a major pain.

  3. Jeremy Kemper on 31 Aug 21:41:

    Jonathan, nothing was killed in the making of this feature.

    This feature is for those with complex applications, plugin developers, and Rails extension artists. Not your everyday just-need-an-app developer.

    So patch up that mortal wound and carry on with nice, readable controllers!

  4. Daniel Lopes on 01 Sep 14:39:

    Really great new feature, thanks for great job José Valim.

  5. Guoliang Cao on 01 Sep 19:37:

    Pagination is very important. If I don’t want to create responder, is it possible to do pagination in controller? Responder is for supporting multiple formats I guess, I don’t need it in most controllers.

  6. Andrew Vit on 04 Sep 16:05:

    In your MyResponder example, you include three modules, each of which defines to_html… don’t those duplicate methods get clobbered, or does it chain them somehow?

  7. gu on 08 Sep 00:29:

    I like all of it, except for the setting of the instance variable in paginated_scope. It’s a bit opaque to set that instance variable in ActionController. Wouldn’t it be better to use an actual scope on the model?

    @Andrew, all the to_html methods call super, so they’re chained in inverse order of inclusion.

  8. RobL on 10 Sep 07:01:

    I am torn between liking the idea but effectively DRYing out everything to the point of not being able to read it concerns me.

  9. Robert on 10 Sep 22:31:

    Cool!

    Although, I’m getting “undefined local variable or method ‘flash’ for #<myresponder:0x1041d9840>” each time a perform an action.

    Any ideas?