Tutorial
Fundamentally, Haml User Tags enables three things:
- Calling Ruby helper functions using Haml syntax
- Creating new helper functions that return Haml blocks
- 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
The attributes work as expected, so user tags can receive content and a hash of attributes:
%MyHelper{foo: "bar"} content
:foo
, but was translated to a string "foo"
when the helper function ran. Haml User Tags will use HashWithIndifferentAccess when running under Rails, so this is not generally an issue.
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 {"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
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:
- The classes and IDs on the tag are combined to form a hash
- A hash literal can be provided between brackets
- More generally, any Ruby expression can be provided (the reference refers to "attribute methods")
- More than one hash can be provided by comma-separating them
-
These hashes are then intelligently combined to form the final attributes, left-to-right
- Multiple
id
values will be concatenated with underscore (_
) - Multiple
class
values will be concatenated with space ( - Multiple
data
values will be merged - All other attributes are merged
- Multiple
%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 {"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