We recently rolled out a new feature in Widgetfinger that allows you to quickly build navigational menus
{% navigation %}
{% link About %}
{% link Services %}
{% link Contact %}
{% endnavigation %}
The above tags will create the following navigation HTML.
As we’ve mentioned before, Widgetfinger uses Liquid, which provides safe templates that don’t affect the security of the server they are rendered on.
Liquid allows you to write custom tags, and that’s exactly what the new navigation tag in Widgetfinger is.
Writing tags in Liquid is fairly straightforward, once you get the hang of it. Lets talk a look at what goes into the navigation tag.
Liquid provides for two different types of Tags, a non-block tag, and a block tag. Since our navigation tag has a starting and ending tag, with other tags inside of it, that’s a block tag, so that’s what we’ll be implementing.
When the Liquid template is parsed, an instance of our Navigation block tag is initialized
class NavigationBlock < Liquid::Block
include LiquidExtensions::Helpers
attr_accessor :links
def initialize(name, params, tokens)
@links = []
super
end
end
In the initialize method, the name
is the name of the tag, and params
is the
extra stuff given to the tag. The navigation tag doesn’t have anything extra
given to it, but the {% contactform :to email@example.com %}
tag in
Widgetfinger does (the :to email@example.com
would be given in the params as a
string). Finally, the tokens
are all of the other tags that appear within
this block tag, including the closing endnavigation
tag.
In the initialize method above for the navigation tag, we simply initialize the
links i-var and call super. The Liquid::Block
initialize method calls
parse
, which parses each of the tokens, causing each of the tags within the
block to be parsed. This means that any valid liquid tag can appear inside your
block. If parse comes across any tag that it doesn’t recognize, it calls an
unknown_tag
method on your Block, allowing you to handle it as you see fit.
Here is the unknown_tag
method for the Navigation block.
def unknown_tag(name, params, tokens)
if name == "link"
handle_link_tag(params)
else
super
end
end
What we’re doing here is pretty straightforward. The only custom tag that we
want to provide within the Navigation tag is the link
tag. So, when check to
see whether the tag is the link tag, otherwise, we call the unknown_tag
method
in the base class. If know handler is ever found for a tag, that’ll cause a
Liquid::SyntaxError
exception to occur. The handlelinktag method gets a
little more interesting, as it provides the meat of the additional parameters
you can pass a link tag.
def handle_link_tag(params)
args = split_params(params)
element_id = args[0].downcase
if args.length > 1
match = (args[1].first == "/" ? args[1][1..-1] : element_id)
@links << { :name => args[0], :match => match, :url => args[1],
:id => element_id, :extra_class => args[2]
}
else
@links << { :name => args[0], :match => element_id,
:url => "/#{element_id}", :id => element_id
}
end
end
def split_params(params)
params.split(",").map(&:strip)
end
In the code above, we’re taking all of the parameters passed to link tag. If there is only one, then we’re going to use several sensible defaults to build the navigation element. If there is more then one, then tag defaults are being overridden. For more information on the addition parameters of the link tag, view the Widgetfinger documentation on it.
Finally, once everything is parsed, and the template is going to be outputted,
the render
method on the Block is called. Here is what the render method
looks like for the Navigation tag.
def render(context)
render_erb(context,
'editor/navigation.rhtml',
:links => @links,
:registers => context.registers)
end
The render
method receives a
Context. This is
provided by Liquid and the Context and its registers
are essentially a hash
where you can store things that’ll be passed around for the parsing of the
template. That’s an over simplification, but it should suffice for our purposes
here.
The render_erb
method is provided by LiquidExtensions::Helpers
, which you
may have noticed that we included above. This is something we devised in order
to open it up so Widgetfinger tags would be able to render Erb, and have access
to the normal Rails view helpers. Here’s how it works.
def render_erb(context, file_name, locals = {})
context.registers[:controller].send(:render_to_string, :partial => file_name,
:locals => locals)
end
After floundering around for a while trying to get Erb Rendering to work by
doing it manually, using Erb directly, and then having to deal with making the
Rails view helpers available in that Erb, we realized that we could just add the
Widgetfinger controller responsible for causing the Liquid templates to be
parsed to the Liquid context registers. From that controller, we can simple
call :render_to_string
on it. This allows us to make regular Rails partial
that are responsible for the output of the tags, that have access to all of the
normal view helpers we’re used to.
In the case of the Navigation tag partial, we’ve composed a hash of links to draw, and the partial outputs it as we expect.
<% links.each do |link| -%>
* <%= match_class registers, link %><%= extra_class link %>">
<%= link_to link[:name], link[:url] %>
Finally, in order for Liquid to know about our new navigation tag, we have to register it like this.
Liquid::Template.register_tag 'navigation', NavigationBlock
Lets talk about Testing
In Widgetfinger, the tags are placed in lib/liquid_extensions.rb
, and we
provide unit tests for this code in test/unit/liquid_extensions_test.rb
.
I won’t cover the full extent of the tests here, but to provide an example of a basic test case that ensures that the proper erb file is rendered.
context "a NavigationBlock tag" do
setup do
@tag = LiquidExtensions::Classes::NavigationBlock.new 'navigation', '',
['{% endnavigation %}']
end
should "render in erb the navigation element when sent render" do
context = stub(:registers => stub)
@tag.expects(:render_erb).with(context,
'editor/navigation.rhtml',
:links => [],
:registers => context.registers).returns ''
@tag.render context
end
end
In the test code above, we instantiate an instance of the navigation block tag, with the proper tag name, no parameters, and the ending tag token.
The test then provides a stub context with a stub registers. We then create an expectation that when the render Erb method will be called with the expected arguments: the proper partial, an empty links array (because there are no links in the tag, as evidenced by the lack of links in the tokens, and the mock registers.
We then call the render
method on the tag, which causes the whole thing to go
into action.
Now that you know how to properly instantiate the tag, and stub out some important pieces of it, providing additional unit tests for the other portions of the tag, particularly those relating to handling the link tags inside of it should be clear.