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?
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.
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 hereActionController::Filters
– custom filter append and prependActionController::Filters::FilterChain
– customized filter append and prepend implementationsActionController::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 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 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 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.
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.
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.
7 Comments
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.
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.
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
Where do I put this code?
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.
Nice page design! May I suggest putting the code on github and adding years to your post dates.
Great article, was just looking for… this :)
Besides, your website is simply amazing…
Keep up the excellent work guys!