Ruby on Rails before_render filter

You probably have used, or at least seen, ActionController::Filters used in lots of Ruby on Rails based applications.
Usually it’s related to some authentication/authorization, or benchmarking or something similar with before_filter and after_filter all over the place.
But what if you could define a filter to be executed after the controller action is invoked and BEFORE the view is rendered?

class UsersController > ApplicationControllerasd
  before_render :inject_my_action_dependent_variables
  ...
end

But why….

You might be asking that why on earth would you need to hook into the dispatch loop right in that spot? Well, lets say i want to execute my controller action, and based on the results of that execution, make some instance variables available in my views. You cannot do that with after filter, as the view is already rendered at that point. And you certainly cannot do it with a before filter.
For instance you could set the page title for each action, without having to define it in the view or having an instance variable set in each action.
We use the filter in conjunction with a sugar-flavored DSL for defining page titles on controllers, which in turn creates before_render filters for each action dynamically and injects the page title variable inside the view before its rendered.

class BlogsController > ApplicationController
  set_title do
  action :show, Proc.new{ [@user.name, "page.titles.blogs.show"] }
  action :index, "page.titles.blogs.index"
  end
  ...
end

That Proc is there because you will get an error for trying to access @user.name before it even exists at action invocation.
Are You sold with the idea of a before_render filter yet?

One for me too, please!

Now that we have agreed that the filter might even be a good idea, there is just one small problem left: filters in Rails don’t seem to be built for extending. This means a healthy amount of monkey-patching and
using alias_method_chain.
So I’ll put on my monkey-patching hat and get some coding going!

Use the source, Luke

After studying the Rails source for a while, i found four key points that need to be replaced/implemented for my own custom filter.

  • ActionController::Base.render – before render filters have to be called here
  • ActionController::Filters – custom filter append and prepend
  • ActionController::Filters::FilterChain – customized filter append and prepend implementations
  • ActionController::Filters::Filter – the base class for all filters needs to be modified a bit

I will start out with the simple stuff: Filter and RenderFilter.

module ActionController
  module Filters

    # compatibility by adding this method to the original Filter class
    class Filter
      def render?
        false
      end
    end

    # define my own custom RenderFilter
    class RenderFilter < Filter
      def type
        :render
      end

      def render?
        true
      end
    end

  end
end

Defining your custom filter append, prepend, skip and compatibility getter methods is pretty straightforward.

module ActionController
  module Filters
    module ClassMethods

      def append_render_filter(*filters, &block)
        filter_chain.append_filter_to_chain(filters, :render, &block)
      end

      def prepend_render_filter(*filters, &block)
        filter_chain.prepend_filter_to_chain(filters, :render, &block)
      end

      # create some alias methods for convenience
      alias :render_filter, :append_render_filter
      alias :before_render_filter, :append_render_filter
      alias :before_render, :append_render_filter

      def skip_render_filter(*filters)
        filter_chain.skip_filter_in_chain(*filters, &:render?)
      end

      def render_filters
        filter_chain.select(&:render?).map(&:method)
      end

    end
  end
end

Filters are applied in a set order: after filters, then around filters and finally after filters. We need to insert our filter after around filter and before filter. This means that the implementation on position finding for a filter in the filter chain needs to be re-implemented, along with couple of other methods.

module ActionController
  module Filters
    class FilterChain

      # helper class attribute for calculating filter insertion position
      cattr_accessor :filters_order
      self.filters_order = [:before, :around, :render, :after]
 
      # new append implementation
      def find_filter_append_position_with_render(filters, filter_type)
        position = self.filters_order.index(filter_type)
        return -1 if position >= (self.filters_order.length - 1)

        each_with_index do |filter, index|
          if self.filters_order.index(filter.type) > position
            return index
          end
        end

        return -1
      end

      # new prepend implementation
      def find_filter_prepend_position_with_render(filters, filter_type)
        position = self.filters_order.index(filter_type)
        return 0 if position == 0

        each_with_index do |filter, index|
          if self.filters_order.index(filter.type) >= position
            return index
          end
        end

        return 0
      end

      def find_or_create_filter_with_render(filter, filter_type, options = {})
        update_filter_in_chain([filter], options)

        if found_filter = find(filter){ |f| f.type == filter_type }
          found_filter
        else
          filter_kind =
            case
              when filter.respond_to?(:before) && filter_type == :before
                :before
              when filter.respond_to?(:after) && filter_type == :after
                :after
              when filter.respond_to?(:render) && filter_type == :render
                :render
              else
                :filter
            end

          case filter_type
            when :before
              BeforeFilter.new(filter_kind, filter, options)
            when :after
              AfterFilter.new(filter_kind, filter, options)
            when :render
              RenderFilter.new(filter_kind, filter, options)
            else
              AroundFilter.new(filter_kind, filter, options)
          end
        end
      end

      # create alias chains for the overriden methods
      alias_method_chain :find_filter_append_position, :render
      alias_method_chain :find_filter_prepend_position, :render
      alias_method_chain :find_or_create_filter, :render
    end
  end
end

The only thing missing now is the part which actually invokes the filters before rendering.

module ActionController
  class Base
    def render_with_render_filter(options = nil, extra_options = {}, &block)
      self.class.filter_chain.select(&:render?).
      map{ |filter| filter.call(self) }
    end

     alias_method_chain :render, :render_filter
  end
end

What now?

To test out this fresh-baked RenderFilter you just have to require this file or files (depends how you decided to organize your code) and define a before_render :foobar in your controller just like you would use any other Rails dispatch filter.

There is at least one “gotcha” i discovered while developing and testing this rendering filter with blocks: your code block will not automatically be invoked in the controller instance context.

class UsersController < ApplicationController
  def index
   @users = User.all
  end

  # this will work
  render_filter :foobar

  def foobar
   @users_count = @users.count
  end

  # this will end up whining that @users is nil
  render_filter do |controller|
    @users_count = @users.count
  end

  # this is what you have to do
  render_filter do |controller|
  # note the block, as regular parameter will be evaluated as a string
    controller.instance_eval{ @users_count = @users.count }
  end
end

Basically when you want to access or set some variables resulting from the action invocation, you have to evaluate your statements in the controller instance context.

If you have any questions, comments or notes about the code, leave a comment.

Tanel Suurhans
Tanel is an experienced Software Engineer with strong background in variety of technologies. He is extremely passionate about creating high quality software and constantly explores new technologies.

7 Comments

  • Ahmed ElDawy

    Good job. Thank you.
    But I still think it’s not necessary to hook “before_render”.
    An alternative solution is to change these instance variables into helper methods and call it directly from the view.
    Example, instead of @user, let it be current_user and move it to ApplicationHelper or BlogHelper. There, you can write the same code that sets @user.

    • Tanel Suurhans

      I guess it’s a matter of preference. In my case, i like the custom tailored DSL a bit more as i have a clear overview of the page titles in my respective controller.
      Your solution would definitely work, but for me it seems a bit messy to have one huge method containing the logic which does conditional checks on n+1 controller names and action names to define the end result.

      The last example with @user is meant more as a very simple example of the concept, the first example containing page titles is the real-world use case in our system.

  • Amiruddin Nagri

    There is a need for a filter like this …

    in my case, I have a webapp where I am using javascript in a progressive enhancement fashion. My webapp works with non-ajaxy request calls to the server. I am trying to convert all of that into ajaxy without breaking the non-ajaxy features as a fallback.

    I have extracted all my views into a partial, so most of my view classes look like :

    index.html.erb

    ‘index’ %>

    The problem is of passing the locals, I thought I can do something like this in a around filter :

    def ajax_request
    before = instance_variable_names
    yield
    set_vars = instance_variable_names – before
    locals = HashWithIndifferentAccess.new
    set_vars.each do |var|
    locals[var.gsub(/@/, '').to_sym] = instance_variable_get(var.to_sym)
    end
    render :partial => params[:action], :locals => locals if request.xhr?
    end

    But as you have pointed out, after_filter is invoked after the view gets rendered and I am getting a DoubleRenderError.

    Any suggestions how I can transform my application to ajax without breaking existing functionality.

    -Amir

  • Matt

    Where do I put this code?

    • yrral86

      I would stick it in lib/ and require it from the controller you want to use it in… or the application controller if you want it available everywhere.

  • bogn

    Nice page design! May I suggest putting the code on github and adding years to your post dates.

  • apneadiving

    Great article, was just looking for… this :)

    Besides, your website is simply amazing…

    Keep up the excellent work guys!

Liked this post?

There’s more where that came from. Follow us on Facebook, Twitter or subscribe to our RSS feed to get all the latest posts immediately.