#397 Action View Walkthrough pro
- Download:
- source codeProject Files in Zip (30.1 KB)
- mp4Full Size H.264 Video (46.2 MB)
- m4vSmaller H.264 Video (25.4 MB)
- webmFull Size VP8 Video (24.3 MB)
- ogvFull Size Theora Video (68 MB)
In this episode we’ll continue the walk through the Rails source code that we started in episode 395 this time looking at the view layer. What exactly happens when Rails renders a template in response to a request? The first thing we’ll do is try to get some context as to where our code is being executed in the Rails stack so we’ll modify the template for a page in a Rails application to display the current class.
<h1>Articles</h1> <%= self.class %> <div id="articles"> <%= render @articles %> </div> <p><%= link_to "New Article", new_article_path %></p>
When we load this page now we’ll see what looks like an anonymous class which isn’t very helpful. If we change this to get the current class’s superclass instead we’ll get a more useful result.
Now we’re getting somewhere. The code inside our layout file is executed in the context of ActionView::Base; where is this in the Rails source code? We can get the Rails source code by cloning its Git repository by running the following command. We’ll then move into the rails directory and check out the branch of the code that matches the version of Rails that our application is using.
$ git clone https://github.com/rails/rails.git $ cd rails $ git checkout v3.2.9
The view and controller code is under the actionpack/lib directory and in here we’ll find the ActionView::Base class. This class includes a number of modules, three of which are Helpers, ::ERB::Util and Context. The Helpers module includes a large number of other modules that contain all the helper methods that we use all the time in our views, while ::ERB::Util contains code to handle escaping, such as HTML escaping. The Context module is rather interesting. According to its documentation it can be used in another class that we want to act as a view context beside ActionView::Base. There aren’t many examples of this in action but it seems like an interesting idea.
Following a Request
Now that we have some idea as to where the view code is located we’ll take a look at what happens when a request comes in to our index action. In episode 395 we walked through what happens in the controller layer when a request comes in. In the AbstractController::Rendering module we have a render method which is where the controller and view layers meet.
def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) end
It’s this method that renders the template and sets it to the body of the response. Before it does the rendering it generates some options by calling _normalize_render. This method calls _normalize_args and if we pass it a symbol or a string it will convert it to an :action option. It then calls _normalize_options which in most cases will add :template and :prefixes options to our render call.
Lookup Contexts, View Contexts, View Renderers
Let’s go back to our Rails app to see what this is doing. We saw in episode 395 that if there’s no call to render in an action it’s called implicitly without any arguments. In the case the :template option is set to the name of the action and :prefixes is set to the name of the controller and its superclasses, in this case to %w[articles application]. This means that render will look for an index template in the articles directory or in the application directory if this doesn’t exist. Back in the render method in the Rails source code after we normalize the options we call render_to_body and this calls _render_template.
def _render_template(options) #:nodoc: lookup_context.rendered_format = nil if options[:formats] view_renderer.render(view_context, options) end
This method first clears the lookup_context.rendered_format if a :formats option exists, then calls render on a view renderer, passing in a view context. There are three different objects that we’re introducing here: view_context, view_renderer and lookup_context and these are all related to rendering out the view. We’ll look at each one, starting with the view_context.
This instantiates a new view_context_class which calls ActionView::Base.prepare. This method calls Class.new which creates a new anonymous class and as we pass self to it it will create a subclass of ActionView::Base. This is the anonymous class that we saw at the beginning of this episode when we inspected it in the template.
def prepare(routes, helpers) #:nodoc: Class.new(self) do if routes include routes.url_helpers include routes.mounted_helpers end if helpers include helpers self.helpers = helpers end end end
This class inherits all the behaviour of ActionView::Base and also includes a few modules passed in from the controller. This gives us some custom routing and helper behaviour so that each controller can have its own context that has specific methods in it. When the view_context_class is instantiated we pass in a few arguments, including one called view_assigns. This is where we transfer the instance variables from the controller to the view.
def view_assigns hash = {} variables = instance_variable_names variables -= protected_instance_variables variables -= DEFAULT_PROTECTED_INSTANCE_VARIABLES variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) } hash end
Here we grab the instance variable names from the controller and assign their values into a hash. This is passed in to ActionView::Base when we instantiate it and an assign method is then called which sets instance variables for each value in that hash. Some developers don’t like the fact that Rails does this as it goes against the grain of objects but it’s good to know that the code involved isn’t too complex.
Now that we have some idea as to how a view context is set up we’ll take a look at the lookup_context next. This method is defined in the view_paths module and it creates a new ActionView::LookupContext, passing in the view paths along with a few other details.
def lookup_context @_lookup_context ||= ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes) end
To get a better understanding as to what view paths are we’ll take a look at this in the Rails console. If we call view_paths on our ArticlesController we get a PathSet object back.
>> ArticlesController.view_paths => #<ActionView::PathSet:0x007fc2e1f49088 @paths=[/Users/eifion/blog/app/views]>
This object includes the paths to the views directory for that controller and there are ways to extend this and add other directory paths and episode 269 has more details on how to do this.
What does a LookupContext do? This class is defined in the action_view directory and as its name implies it’s used to look up templates so it makes sense that we need to pass our view paths into it. We’ll look at this class later when we look at rendering a template. Speaking of rendering let’s go back to the Rendering module and take a look at the third piece of the puzzle: ActionView::Renderer. We pass in the lookup context when this is instantiated and this is the object that we call render on when we call render in a controller.
# Main render entry point shared by AV and AC. def render(context, options) if options.key?(:partial) render_partial(context, options) else render_template(context, options) end end
The behaviour for rendering a partial and for rendering a template is quite different so this split is made early on. The render_template method calls render on a TemplateRenderer class. There’s quite a lot of code in that method but it basically fetches a template object then renders that template. Let’s see how we fetch the template first. This is done in a determine_template method in TemplateRenderer.
def determine_template(options) #:nodoc: keys = options[:locals].try(:keys) || [] if options.key?(:text) Template::Text.new(options[:text], formats.try(:first)) elsif options.key?(:file) with_fallbacks { find_template(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, :locals => keys) elsif options.key?(:template) options[:template].respond_to?(:render) ? options[:template] : find_template(options[:template], options[:prefixes], false, keys, @details) else raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file or :text option." end end
The behaviour in this method changes a lot depending on the options that are passed in. We’ll focus on the code that’s executed if a :template option is passed in as this is the option that’s automatically added when we normalize the render options from the controller action. This code calls find_template and this method is delegating to the lookup context. This makes sense since that is designed to fetch a template. In LookupContext find_template is delegated to find which uses the view paths to find it.
def find(name, prefixes = [], partial = false, keys = [], options = {}) @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :find_template :find
The @view_paths variable is set to an instance of PathSet and this class’s find method looks for all the templates and fetches the first one. If it doesn’t find any it raises a MissingTemplate exception.
def find(*args) find_all(*args).first || raise(MissingTemplate.new(self, *args)) end def find_all(path, prefixes = [], *args) prefixes = [prefixes] if String === prefixes prefixes.each do |prefix| paths.each do |resolver| templates = resolver.find_all(path, prefix, *args) return templates unless templates.empty? end end [] end
The find_all method here loops through all the different paths that are available and returns a resolver which we call find_all on. The Resolver class lives in the template directory and its find_all method looks for a cached template that matches the details passed in and calls find_templates if it fails to find one.
# Normalizes the arguments and passes it on to find_template. def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details) end end
The find_templates method raises a NotImplementedError if it’s called unless we’re calling it on a subclass, which is what we’re doing in our Rails application. The resolver can fetch templates in different ways depending on the class that we’re using and the Crafting Rails book has an example of how we can make a custom resolver. This method is also defined in the PathResolver class which inherits from Resolver and which is in the same file as it. This calls query which is finally the code that reads the contents of the template file and creates a Template object. We pass the file’s contents into this object and also a handler object which will interpret the template. Rails comes built-in with a couple of handlers for erb and XML builder but we can create our own as we demonstrated in episode 379.
Now that we have our template object let’s quickly review what it took to get it. Our template renderer delegated to the lookup context which in turn delegated to the path set and this delegated to the resolver. The code goes through quite a few layers but this allows for some flexibility in which layers we can swap out if we want to change some of the lookup behaviour.
Let’s go all the way back up the chain to our template renderer. We now know that its find_template method ends up returning a Template object. Once we have this object we can render it and this is done in a render_template method. Before we attempt to render the template we first try to render it with a layout.
def render_template(template, layout_name = nil, locals = {}) view, locals = @view, locals || {} render_with_layout(layout_name, locals) do |layout| instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do template.render(view, locals) { |*name| view._layout_for(*name) } end end end
To fetch a layout render_with_layout we call find_layout which calls resolve_layout and this fetches a layout in one of a variety of ways, most of which call find_template. A layout template is fetched in the same way as a normal template and once we’ve fetched it we yield to the block that’s passed in which will render the template first and then the layout. The content of the rendered template goes into the layout wherever it calls yield without an argument being passed in.
Next let’s find out what the render method on the Template object does. The rendering behaviour is split into two parts. First the template is compiled which generates a new method on the view context. This method is then called on the view. This is done for performance reasons.
def render(view, locals, buffer=nil, &block) ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do compile!(view) view.send(method_name, locals, buffer, &block) end rescue Exception => e handle_render_error(view, e) end
The compile! method ends up calling a compile method but before that it checks to see which module it should add the method to. Ours will be added to ActionView::CompiledTemplates which is a module that holds the compiled template code which is the module we’re adding the compile method to.
if view.is_a?(ActionView::CompiledTemplates) mod = ActionView::CompiledTemplates else mod = view.singleton_class end compile(view, mod)
This module is passed to the compile method and this calls @handler.call. If you’re familiar with template handlers you’ll know that this should return some Ruby code that needs to be compiled into a method. This code is added to the end of a dynamic method definition and this method is then added to the module.
def compile(view, mod) #:nodoc: encode! method_name = self.method_name code = @handler.call(self) source = <<-end_src def #{method_name}(local_assigns, output_buffer) _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} ensure @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer end end_src # rest of method omitted. end
The name of the compiled method is determined in the method_name method.
def method_name #:nodoc: @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_") end
This will look familiar if you’ve ever seen an exception raised in the view. The method name is long and includes the name of the template and it’s here where it’s generated.
Back in the render method after compile! is called we call view.send and pass in the name of the generated method. It’s this that renders out the template and returns the string of content. The code defined in the compiled method it determined by the handler, in this case an erb template. The string of rendered content is returned in the template renderer from the render method and is eventually set to the response_body in the Rendering module.
There’s quite a bit going on behind the scenes when a request comes in to a Rails application and a template is rendered, but as there’s a lot of caching built into the framework so this all works fairly quickly. One thing we haven’t covered in this episode are the helper methods that can be called in a template such as render or link_to, but these are easy to explore on your own.


