As is fairly well documented by now, I like Rails, and I’ve done quite a bit of stuff in it. However, through a brief stint of work on The Carbon Account I’ve been getting my feet wet with arch-rival (ok, not really) framework Django, and I found myself hugely impressed with its templating system. A less stubborn person than me would think “That’s a nice idea. Maybe I should think about switching to Django.” My reaction, however, is “That’s a nice idea. I think I’ll steal it.”
To borrow an example from the Django template documentation, the big selling point is that you can take a base template like this (base.html):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<link rel="stylesheet" href="style.css" />
<title>{% block title %}My amazing site{% endblock %}</title>
</head>
<body>
<div id="sidebar">
{% block sidebar %}
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog/">Blog</a></li>
</ul>
{% endblock %}
</div>
<div id="content">
{% block content %}{% endblock %}
</div>
</body>
</html>
and build specialised versions of it by overriding as few or as many as those {% block %}s as you like:
{% extends "base.html" %}
{% block title %}My amazing blog{% endblock %}
{% block content %}
{% for entry in blog_entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.body }}</p>
{% endfor %}
{% endblock %}
In Ruby land, that would translate to something like:
<% extends "base.html" %>
<% block :title do %>My amazing blog<% end %>
<% block :content do %>
<% for entry in blog_entries %>
<h2><%= entry.title %></h2>
<p><%= entry.body %></p>
<% end %>
<% end %>
Seasoned rubyists will notice that this is built on top of ERB. Well, no point in reinventing the wheel.
Actually coding that up was easier said than done, though. For a start you’ve got to inject the ‘extends’ and ‘block’ methods into an execution context that you don’t have an awful lot of control over, and then you’ve got to deal with ‘block’ changing its behaviour based on whether you’ve encountered an ‘extends’ line yet. In the end the experience was not unlike writing a program in Muriel, the esoteric language I wrote where the only control structure is “dynamically generate another program to execute”. See, I knew that would come in useful one day. But enough of that – here’s the code:
require 'erb'
module Djerb
# Evaluate the file 'filename' as a Djerb template, in the context of the
# binding 'b'
def Djerb.run(filename, b = TOPLEVEL_BINDING)
# initialise the 'global' definitions @_djerb_blocks, block and extends
# (which should persist between templates), then hand off to continue
eval <<-ENDDEFS, b
@_djerb_blocks = {}
def block(id, &block_proc)
# define the block identified by 'id' if it's not already been defined
# by a more specific template, and immediately evaluate it if this is
# the base template (i.e. we haven't seen an 'extends' yet).
@_djerb_blocks[id] ||= block_proc
@_djerb_blocks[id].call if @_djerb_evaluate_blocks
end
def extends(filename)
# Schedule 'filename' to be executed after we've finished
# processing the remaining declarations in the present template
@_djerb_next_template = filename
@_djerb_evaluate_blocks = false
end
ENDDEFS
continue(filename, b)
end
# Evaluate the Djerb template 'filename', inside a binding which has been
# pre-seeded by run
def Djerb.continue(filename, b = TOPLEVEL_BINDING)
eval <<-ENDDEFS, b
@_djerb_next_template = nil
@_djerb_evaluate_blocks = true
ENDDEFS
ERB.new(File.read(filename)).result(b)
eval <<-ENDRESULT, b
# if another template has been named by 'extends', evaluate that one next,
# otherwise _erbout is our final result
@_djerb_next_template ? Djerb.continue(@_djerb_next_template, binding) : _erbout
ENDRESULT
end
end
Invoke it with this incantation:
require 'djerb'
puts Djerb.run('blog.html')
So there you go. Possibly not the most elegant way of doing it (it pollutes namespaces like a big polluting chimney, and I haven’t dared to prod it too hard yet for fear that it’ll fall over completely) but it’s the best I could come up with, and it implements a Really Neat Idea in 40ish lines of code, which has got to count for something. Coming up with a version that still works while not making small children cry is left as an exercise for the reader.
This is awesome. Have you continued to use this in your apps? I’d love to see this become more mainstream. Django templates are my favorite templating system too.
I haven’t used this anywhere in production, actually… I’ve just switched to Django instead :-)
Nice solution! While doing some research on the subject, I found the “content_for” method (see http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-content_for ). I guess they added it in a later version of Rails :)