Tutorial

Fundamentally, Haml User Tags enables three things:

  1. Calling Ruby helper functions using Haml syntax
  2. Creating new helper functions that return Haml blocks
  3. Including tags from one template file in another template file

This tutorial will take a close look at what these things mean, and then talk about they can be used effectively.

Call Ruby helper functions using Haml syntax

Haml User Tags adds a special syntax to the Haml compiler that insructs Haml to run a helper method instead of outputting a tag. Imagine a Ruby helper method like this:

def MyHelper(attributes = {})
  content = capture_haml {yield} if block_given?
  return "<samp>MyHelper called with #{attributes.inspect} and #{content.inspect}</samp><br />\n"
end

The first thing to notice is that this method is named differently than most Ruby methods. Haml User Tags are required to begin with a capital letter and be CamelCased. Like a typical Ruby helper function, it receives a hash of options and accepts a block of content.

Now this helper method can be called from Haml:

%MyHelper
MyHelper called with {} and nil

The attributes work as expected, so user tags can receive content and a hash of attributes:

%MyHelper{foo: "bar"} content
MyHelper called with {"foo"=>"bar"} and "content"

In addition to specifying attributes as a hash, Haml also knows how to translate IDs and class names:

%MyHelper.cls1 with class name
%MyHelper#id with ID
%MyHelper.cls1{class: "cls2"} merging multiple classes
MyHelper called with {"class"=>"cls1"} and "with class name"
MyHelper called with {"id"=>"id"} and "with ID"
MyHelper called with {"class"=>"cls1 cls2"} and "merging multiple classes"

Create new helper functions that return Haml blocks

Calling helper methods from Haml is useful, but only half of what's needed to create composable tags. In order to create Haml tags that are composed of other Haml tags, Haml templates must be able to create these helper methods directly. To enable this, Haml User Tags provides a helper method, define_tag. It works like this:

- define_tag :MyHamlHelper do |attributes, content|
  %samp MyHelper called with #{attributes.inspect} and #{content.inspect}
  %br

%MyHamlHelper.cls helper defined in Haml directly
MyHelper called with {"class"=>"cls"} and "helper defined in Haml directly"

All define_tag does is create a new helper method on the template object, so it works basically the same as MyHelper, above. User tags can do everything that normal Haml templates can do, including accessing instance variables and calling other user tags.

Include tags from one template file in another template file

Haml User Tags provides a method called include_tags that takes a path to a template file, and will copy the tags defined in that template into the calling one. For example, if there were a file called _bootstrap.html.haml and it defined a tag called Alert, then it could be accessed like this:

- include_tags "_bootstrap.html.haml"

%Alert.alert-info The Alert tag was imported!

Because the user tags are helper methods that are copied into the current template, they share the same self object and instance variables. This is the same behavior as Rails partials.

The include_tags method can also be called from Ruby code, which allows helper modules that are shared across a project to be created. Consider the following helper module:

module BootstrapHelper
  extend HamlUserTags::Helpers
  include_tags "_bootstrap.html.haml"
end

Now in all templates that extend this module, all of the custom tags defined in the imported template are available immedately. This also has a performance benefit, since the included file is only compiled once and the results are shared amongst all templates that include the module.

Rails-specific features

In Rails, include_tags does not take a filename. Instead, it searches for a partial in views/helpers/. For example, given the following helper module:

module ApplicationHelper
  extend HamlUserTags::Helpers
  include_tags "application/bootstrap"
end

Rails will expect to find the file in app/views/helpers/application/_bootstrap.haml.

Composition

With these basic building blocks, it is now possible to create composable tags. This section of the tutorial describes techniques and conventions that will be useful while building a library of user tags in an app.

To begin, here is a simple LinkButton tag that renders a Bootstrap-themed button to a given link.

- define_tag :LinkButton do |attributes, content|
  %a.btn.btn-default{href: attributes["href"]}= content
%LinkButton{href: "#"} My Button
%LinkButton.btn-primary{href: "#"} Primary Button

This user tag has some obvious limitations: a class is given to the second button, but because the method did not explicitly pass the class attribute on, it was left out of the result. Fortunately, Haml allows passing entire hash objects to tags instead of specifying each key-value pair:

- define_tag :LinkButton do |attributes, content|
  %a.btn.btn-default{attributes}= content
%LinkButton{href: "#"} My Button
%LinkButton.btn-primary{href: "#"} Primary Button
%LinkButton{href: "#", title: "Title text"} Another Button

This technique is known as attribute forwarding. Because all attributes are forwarded on to the underlying <a> element, the class even worked with the title attribute. In this way, the LinkButton can support all of the attributes of the tag it is composed of (<a>).

Consider a new type of LinkButton that opens a popover:

- define_tag :PopoverButton do |attributes, content|
  - title = attributes.delete "title"
  - label = attributes.delete "label"
  %LinkButton.popover-button{attributes, :data => {toggle: "popover", title: title, content: content}}
    = label || "Show Popover"

%PopoverButton.btn-primary{title: "Popover content"}
  This content appears inside of the popover.

Now PopoverButton supports all of the features of LinkButton (and thus <a>). Notice the pattern of calling attributes.delete, which removes the key from the hash and returns the value if it was set. This is necessary because otherwise the LinkButton would end up with title and label attributes, which is not intended. In this way known attributes are handled and handling of unknown attributes is deferred to the constituent tags.

Consider this new LinkButton tag that supports a new feature: adding an icon to the button.

- define_tag :LinkButton do |attributes, content|
  - icon = attributes.delete "icon"
  %a.btn.btn-default{attributes}
    - if icon
      %span.glyphicon{class: "glyphicon-#{icon}"}
    = content
%LinkButton{href: "#", icon: "star"} My Button
%PopoverButton{title: "An Icon", icon: "glass"}
  This button has an icon, even though the code for
  PopoverButton did not change.

Even though PopoverButton has no knowledge of the icon attribute, it already supports it.

Attribute forwarding

Attribute forwarding is a simple and powerful way to compose HTML tags. It allows user tags to inherit behavior without inheriting implementation.

This section will go over some of the subtleties of attribute forwarding. Most of this is the behavior described in the Haml reference, but since it is such an integral part of building composable tags, it's worth reviewing the capabilities here. The Haml attribute syntax has the following traits:

%MyHelper#id1{id: "id2"}
%MyHelper.class1{class: %w{class2 class3}}
- hash = {class: "class1", data: {foo: "bar"}}
%MyHelper{hash, class: "class2", data: {baz: "qux"}}
MyHelper called with {"id"=>"id1_id2"} and nil
MyHelper called with {"class"=>"class1 class2 class3"} and nil
MyHelper called with {"class"=>"class1 class2", "data"=>{:foo=>"bar", :baz=>"qux"}} and nil

Lazy Evaluation

The content of user tags is not evaluated until the content parameter is accessed. This allows user tags to set up context for their child content. In the following example, this technique is used to set an instance variable describing the form that is currently being built. This technique enables the creation of simple wrappers around the Rails form builders.

- define_tag :Form do |attributes, content|
  - @form_for = attributes["for"]
  %form= content
- define_tag :TextField do |attributes, content|
  - field = attributes.delete "id"
  %input{attributes, name: "#{@form_for}[#{field}]", placeholder: field}
%Form{for: :some_model}
  %TextField#first_name
  %TextField#last_name