GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Custom Tags in Liquid

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.

<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

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 unknowntag method on your Block, allowing you to handle it as you see fit. Here is the unknowntag 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 unknowntag method in the base class. If know handler is ever found for a tag, that’ll cause a Liquid::SyntaxError exception to occur. The handlelink_tag 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 :rendertostring 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.


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/liquidextensions.rb, and we provide unit tests for this code in test/unit/liquidextensions_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.