Custom Tags in Liquid
Posted by Chad Pytel
Oct 03
We recently rolled out a new feature in Widgetfinger that allows you to quickly build navigational menus
1 2 3 4 5 |
{% navigation %}
{% link About %}
{% link Services %}
{% link Contact %}
{% endnavigation %}
|
The above tags will create the following navigation HTML.
1 2 3 4 5 |
<ul id="navigation"> <li class="about"><a href="/about">About</a></li> <li class="services"><a href="/services">Services</a></li> <li class="contact"><a href="/contact">Contact</a></li> </ul> |

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
1 2 3 4 5 6 7 8 |
class NavigationBlock < Liquid::Block include LiquidExtensions::Helpers attr_accessor :links def initialize(name, params, tokens) @links = [] super 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.
1 2 3 4 5 6 7 |
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 handle_link_tag method gets a little more interesting, as it provides the meat of the additional parameters you can pass a link tag.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.
1 2 3 4 5 6 |
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.
1 2 3 |
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.
1 2 3 4 5 6 7 |
<ul id="navigation">
<% links.each do |link| -%>
<li class="<%= link[:id] %><%= match_class registers, link %><%= extra_class link %>">
<%= link_to link[:name], link[:url] %>
</li>
<% end -%>
</ul>
|
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.
Comments on this post
Oct 05
hidden local said,
Great post! But i have a concern… What is wrong with markaby, i really hate erb, it is kinda of nasty…
This :<% links.each do |link| -%>- <= link[:id] %><= match_class registers, link ><= extra_class link %>”>
<%= link_to link[:name], link[:url] %>
<% end -%>
will be ul.navigation! do links.each do |link| li :class => link[:id] do ... end end
and so on, is not it way bettwen than keep typing </>
?
Thanks
Oct 28
James said,
I’d be interested to know what the match_class method looks like, since I can’t figure out how to do/use something like current_page? in a Liquid template. Thanks!
Oct 28
James said,
“we realized that we could just add the Widgetfinger controller responsible for causing the Liquid templates to be parsed to the Liquid context registers”. How? Also, is LiquidExtensions::Helpers just some module that you mix in to your tags?
Oct 29
Chad Pytel said,
James, match_class is just a normal Rails view helper:
Yes, LiquidExtensions::Helpers is just a module we mix into the tags. Are you asking how we add the controller to the registers? If so, we just pass into the template when we render the template.
Oct 30
James said,
Aha, the relevant syntax I was missing was:
Liquid::Template.parse(template).render({ ‘variable’ => value }, :registers => { :controller => controller })
For my implementation I basically copied the if.rb tag from Liquid, making a few modifications to use a custom Condition I added (which inherits from Liquid’s Condition class).
Oct 30
James said,
Oh, and my custom Condition just reimplements current_page? from Rails with a few modifs. So I can now do:
Sorry, comments are closed for this article.
© 2000 - 2008 by thoughtbot, inc.
written by a bushel of tiny robots
Archives
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our training.
Come “ride the toad” on hoptoad, the app error app
Widgetfinger: simple content management for simple websites