#379 Template Handlers pro
- Download:
- source codeProject Files in Zip (62.3 KB)
- mp4Full Size H.264 Video (29 MB)
- m4vSmaller H.264 Video (14.4 MB)
- webmFull Size VP8 Video (17.1 MB)
- ogvFull Size Theora Video (33.2 MB)
Below is a simple view template which displays a list of products. When Rails renders this template it parses it with a template handler and the one it uses is determined by the file extension, in this case erb
.
<h1>Products</h1> <table id="products"> <tr> <th>Product Name</th> <th>Release Date</th> <th>Price</th> </tr> <% @products.each do |product| %> <tr> <td><%= link_to(product.name, product) %></td> <td><%= product.released_on.strftime("%B %e, %Y") %></td> <td><%= number_to_currency(product.price) %></td> </tr> <% end %> </table>
Rails provides another template handler called Builder which was covered in episode 87. This is used to generate XML and is especially useful for creating RSS or Atom feeds. There are many other template handlers available as gems including HAML and RABL but in this episode we’ll focus on creating our own handler.
Before we do this we’ll address a common source of confusion: the double extension in the filename of a template. It’s important to understand that the first extension is the format of the response while the second is the template handler that will be used to parse the file. The template above will therefore return HTML and be parsed through erb. If the file was called index.html.haml
then it would still return HTML but be parsed by a HAML parser.
A Ruby Template Handler
We’ll start by making a new template file to demonstrate the template handler we want to create. We want our new template to return JSON and use Ruby as its template handler and so we’ll call it index.json.rb
. This template’s response will be whatever string is returned at the end of the file.
"Hello World!"
When we try this out we see an error message we visit /products.json
.
We see this error as our application can’t find a template with a handler that it understands. We can fix this by making a new template handler for the rb
extension. We’ll do this inside the initializers
directory although if we were doing this in a production application it would be better to extract this functionality out into a gem.
A handler is simply an object that responds to calls so we could just write a lambda here if we wanted to. This lambda needs to take a template object and return a string containing some Ruby code. We’ll add some simple Ruby code to test this handler. It might seem a little odd to put Ruby code in a string like this but this is done for performance reasons. We also need to register the template handler which we do in the second line by calling register_template_handler
and passing in the extension and our handler object.
handler = ->(template) { "Date.today.to_s" } ActionView::Template.register_template_handler(:rb, handler)
We need to restart our application for the new initializer to be picked up. Once we’ve done so when we reload the page we’ll see current the date in the response.
Obviously we don’t want to respond with just the date every time, we want to use the Ruby code in the template. Doing so is quite easy as the template
object that’s passed to the handler responds to a source
method that we can use. This will return a string containing the content of the template file.
handler = ->(template) { template.source } ActionView::Template.register_template_handler(:rb, handler)
After restarting our application again we’ll see the response from the template.
There is a cleaner way to write this handler by passing in the symbol :source
as the second argument and calling to_proc
on it. This will call source
on the object passed into the proc.
ActionView::Template.register_template_handler(:rb, :source.to_proc)
Using Our Template To Generate JSON
Now that we have a working rb
template we can use it to generate some JSON.
@products.map do |product| { name: product.name, price: number_to_currency(product.price), url: product_url(product) } end.to_json
Here we loop through each of the products, map them to an array of hashes and call to_json
on that to convert it to a string. The advantage of doing this here instead of calling @products.to_json
is that we can use helper methods and generate URLs. When we reload the page now we’ll see the products rendered as JSON.
This approach feels much simpler than using RABL or JBuilder to create this page but what about more complex scenarios such as embedding associations into the JSON response? This is actually quite simple to do. Imagine that a product can have many reviews. We can fetch the reviews, loop through them and return another hash from the review that we want to display.
@products.map do |product| { name: product.name, price: number_to_currency(product.price), url: product_url(product), reviews: product.reviews.map do |r| { name: r.name } end } end.to_json
Alternatively if we need the reviews JSON elsewhere in our application we can extract that code out into a partial to reduce duplication. We can then call render
and pass in the review object.
@products.map do |product| { name: product.name, price: number_to_currency(product.price), url: product_url(product), reviews: product.reviews.map { |r| JSON.parse(render(r)) } end } end.to_json
This will look for a partial at reviews/_review.json
and we can use the .rb
template handler again to return a JSON string representing a product’s reviews. As the partial returns a string we need to parse this into JSON here to stop each review being returned as a single JSON string.
Generating A Different Response With Our Template
A Ruby template is useful for more than just generating JSON, it’s also useful for generating CSV responses. To generate a CSV representation of our products we can make an index.csv.rb
template.
CSV.generate do |csv| csv << ["Name", "Price", "URL"] @products.each do |product| csv << [ product.name, number_to_currency(product.price), product_url(product) ] end end
This template uses the standard CSV library and generates a header row and a row for each product. The advantage of this approach over the one we used in episode 362 is that we can use helper methods for generating URLs and so on which wouldn’t be possible if we were generating the CSV inside the model. In order for this to work we need to include the csv
library in the configuration file.
require 'csv'
We can try this out now by visiting /products.csv
. When we do we’ll see the products rendered as CSV.
If we want this file to be downloaded rather than displayed in the browser we can alter the template. We have access to the response
object here and we can change the response headers so that the file is downloaded.
response.headers["Content-Disposition"] = 'attachment; filename=products.csv'
When we visit this URL now the file is downloaded instead of shown in the browser.
Generating HTML
A Ruby template can even be used to generate HTML and we’ll demonstrate this in the show template. The erb
version of this currently looks like this.
<%= div for @product do %> <h1><%= @product.name %></h1> <p><%= number_to_currency @product.price %></p> <%= link_to 'Edit', edit_product_path(@product) %> <% end %>
Normally we’d only consider doing this if the erb tags greatly outweigh the HTML tags in the template. We’ll rename our existing show template from .erb
to .rb
and paste in the Ruby code that’s equivalent to the original erb.
div_for @product do content_tag(:h1, @product.name) + content_tag(:p, number_to_currency(@product.price)) + link_to("Edit", edit_product_path(@product)) end
Here we use helper methods like div_for
and content_tag
to generate the HTML. This isn’t something we’d normally do but it works as an example of what’s possible with a Ruby template.
Parsing Markdown
Next we’ll shift gears and make another template handler, this time one for parsing Markdown. This can be helpful for creating informational pages. We’ll create a handler with an md
extension that handles Markdown templates and create a template for our application’s “About Us” page.
# About Us **Lorem ipsum dolor sit amet, consectetur adipisicing elit,** sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
This kind of template certainly won’t work well in every situation but for pages like this it’s perfect. To get this template to work we need to create another initializer.
class MarkdownTemplateHandler def self.call(template) # Template processed here... end end ActionView::Template.register_template_handler(:md, MarkdownTemplateHandler) ActionView::Template.register_template_handler(:markdown, MarkdownTemplateHandler)
This time we’ve created a MarkdownTemplateHandler
class and given it a class method called call
that takes a template as an argument. This method needs to parse the markdown file that’s passed in and return the resulting HTML. At the bottom of the file we use this class to register the template handler and we create two handlers, md
and markdown
, so that two different extensions supporting Markdown are created. We still need to complete the call
method so that it converts Markdown to HTML. We’ll use Redcarpet for this so we’ll need to add it to our gemfile then run bundle
to install it.
gem 'redcarpet'
Back in our template handler we’ll finish the call
method.
def self.call(template) renderer = Redcarpet::Render::HTML.new(hard_wrap: true) options = { autolink: true, no_intra_emphasis: true, fenced_code_blocks: true, lax_html_blocks: true, strikethrough: true, superscript: true } Redcarpet::Markdown.new(renderer, options).render(template.source) end
All we do here is create a new HTML renderer and pass it some options. We then create a new Markdown parser and use it to parse the template. Once we restart the server to pick up the new initializer we can try visiting our “About Us” page. When we do, however, we get an exception.
It seems that our template handler is trying to parse our HTML source as Ruby code. This is a common problem when working with template handlers. In our call
method we return the HTML as a string but we should be returning some Ruby code in a string so the HTML is evaluated as Ruby code. A quick fix for this is to call inspect on the string so that it’s quoted and considered to be Ruby code.
Redcarpet::Markdown.new(renderer, options).render(template.source).inspect
After another restart and reload the page now works as expected.
The Markdown in the template is now converted to HTML. It would be nice if we could add dynamic content to our Markdown templates using erb to do things like displaying links like this:
[Products Listing](<%= products_path %>)
You might think that adding an erb
extension to this file would fix this but this isn’t possible in Rails views. To get this to work we’ll need to code this functionality into our Markdown template. We could make a separate extension and handler for Markdown with embedded erb but instead we’ll add this functionality to our current handler which involves making a few changes to the file.
class MarkdownTemplateHandler def self.call(template) erb = ActionView::Template.registered_template_handler(:erb) source = erb.call(template) <<-SOURCE renderer = Redcarpet::Render::HTML.new(hard_wrap: true) options = { autolink: true, no_intra_emphasis: true, fenced_code_blocks: true, lax_html_blocks: true, strikethrough: true, superscript: true } Redcarpet::Markdown.new(renderer, options).render(begin;#{source};end) SOURCE end end
The first thing we do now is run our template through the erb template handler. We then run call
on that template handler and pass in our template which returns Ruby source code inside a string. Things get a little tricker from here on. We can’t pass this source code directly into Redcarpet as it’s now Ruby code and this needs to be reevaluated on each request but the call
method isn’t triggered on each request, it’s cached for performance reasons. To solve this problem we need to move all the Redcarpet functionality into a string and we use a heredoc for that. We need to pass the Ruby source code in the source
variable into the call to render
and we wrap it in a begin end
block to do that. We also remove the call to inspect
as we’re now returning Ruby code in a string. When we reload the page now we’ll see the product listing link.
We now have a working Markdown template handler that supports erb. The code is a little messy as we’ve had to put a lot of code into strings but this is a side-effect of how Rails template handlers work. Thankfully once we get this working we won’t have to adjust it much, we can just use it in our views.
On a related note it’s worth taking a look at the Markerb gem by Jose Valim. This goes one step further and uses this to generate multipart emails though a simple Markdown and erb template. He describes this process in more detail in his book “Crafting Rails Applications”. Another gem worth considering is called Tilt. This provides a standardized interface for working with a variety of template engines. This isn’t used in Rails for generating templates but it is used in the asset pipeline and may be covered in a future episode.