Swombat

wombat + s 
« Back to blog

Rails: Rendering templates outside of a controller

There are times when you need to render a view template outside of the context of a controller.

Yes, most of the time, if you think "I want to render this in my mode" you're Doing It Wrong. So, before you do this, think very carefully whether this rendering really belongs outside of a controller. Most of the time, it doesn't.

Here's an example from Woobius. I've been working on a Project Archive functionality. For the project archives to be useful, they should not only include files from the project, but also issue sheets, audit logs, and other meta-information. The result of the archival process is a zip file containing all the files in the project, as well as all that meta-information - ideally in an easily accessible format like, say, HTML.

Archiving a project that might contain several gigabytes of architectural drawings is obviously not something that should be happening in a controller. It's a fire, forget, and receive a DVD in the post a few days later kind of process. Rendering HTML files in a Rails application with anything other than ERB (or HAML, etc if you swing that way) would be silly. So, this is a perfectly legitimate case where we need to render ERB templates outside of the context of a controller.

A long time ago, I worked on an application where I had to rush to figure out how to do this, so I ended up spawning a test-controller from within the console context and using that to render the page. That had a number of issues which I won't get into in this article, but the main one was probably the fact that it was really ugly and nasty - a hack of the wrong kind. 

That was then, and this is now, and I thought I'd figure out the correct way of doing this. Perhaps my Google-fu is lacking, but I didn't find all that much help on this topic, until I stumbled on this page. In short, it gives examples of how to render an ERB in a controller-free context. I thought it'd be quite simple to use lifo's code, but it turned out to have a big problem.

You see, when I render views, I like to use my carefully constructed helpers, not juggle plain ERB and HTML. So I included my helpers:

require 'erb'
 require 'ostruct'
 
 def render_template(template, params)
  vars = OpenStruct.new params
  class << vars
   include CustomHelper, ApplicationHelper, ActionView::Helpers::TextHelper
  end
  template = File.read("#{TEMPLATE_ROOT}/#{template_path}.html.erb")
  puts ERB.new(template).result(vars.send(:binding))
 end

However, getting this to work involved all sorts of 'fun' because of the output_buffer is not provided to the template, by ERB, and so the concat TextHelper method doesn't work. Without concat, most interesting helpers fall on their nose. There follows a lengthy session of working my way through ERB's internals to find out how to expose that output_buffer. It turns out ERB is pure evil stuff (though I'm sure it all made sense at the time). It renders the template into a string, which it then evals. You can set the output_buffer variable name, but try as I might I couldn't get the template to have access to it.

Luckily, though, while I was desperately tracing through the Rails code and doing a lot of global searches, I found a file, compiled_templates_test.rb, which involved rendering some templates. And, lo and behold, in the tests, a completely different rendering method was used:

 assert_equal "Hello world!", render(:file => "test/hello_world.erb")
 def render(*args)
  view_paths = @explicit_view_paths || ActionController::Base.view_paths
  ActionView::Base.new(view_paths, {}).render(*args)
 end
 

Aha!

It turns out, this is by far the easier way to render templates, in a rails application, outside of a controller. So here's the final code, for anyone to reuse as they see fit. Hopefully it will save some other hapless soul a couple of hours of poking around:

 def render_erb(template_path, params)
  view = ActionView::Base.new(ActionController::Base.view_paths, {})
 
  class << view
   include EmailHelper, ApplicationHelper
  end
 
  view.render(:file => "#{template_path}.html.erb", :locals => params)
 end
 

Comments (4)

Jul 22, 2009
Frank said...
Thanks a bunch for that - I really needed exactly this and couldn't find anything that worked for me.
Jul 22, 2009
Rob said...
Nice work! I looked at something similar a while ago when I wanted to use erb templates for system messages in the same way as I do for emails - sadly project deadlines meant I never made it this far :( Thanks for saving me some effort :D
Aug 18, 2009
jamesotron said...
That works perfectly, thanks.
Sep 09, 2009
JohnD said...
Thanks for this, I think it's setting me down the right path, but it's not working for me yet...

I'm getting an error when trying to use the "url_for" helper method in my erb template:

You have a nil object when you didn't expect it! (ActionView::TemplateError)
The error occurred while evaluating nil.url_for

It seems that the constructor you use for ActionView::Base leaves a nil controller, and nil formats, which a number of the common helpers (such as url_for) rely on.

Seems the templating systems calls controller.request.content_type, etc. Each thing I stub out just leads to a new "You have a nil object when you didn't expect it!" error.

I also can't seem to render partials from within a view this way...

So, I guess for simple stuff without common helpers this is okay, but I don't think it's a ready-for-prime-time solution to the original problem.

Any new thoughts or solutions, let me know... I'm about to embark on generation lots of templates...

Leave a comment...

 
Got an account with one of these? Login here, or just enter your comment below.
Posterous-login    twitter