Create custom template tags
Marten has built-in support for common template tags, but the framework also allows you to write your own template tags that you can leverage as part of your project's templates.
Defining a template tag
Template tags are subclasses of the Marten::Template::Tag::Base
abstract class. When writing custom template tags, you will usually want to define two methods in your tag classes: the #initialize
and the #render
methods. These two methods are called at different moments in a template's lifecycle:
- the
#initialize
method is used to initialize a template tag object and it is called at parsing time: this means that it is the responsibility of this method to ensure that the content of the template tag is valid from a parsing standpoint - the
#render
method is called at rendering time to apply the tag's logic: this means that the method is only called for valid template tag statements that were parsed without errors
Since tags are created and processed when the template is parsed, they can theoretically be used to implement any kind of behavior. That being said, there are a few patterns that are frequently used when writing tags that you might want to consider to help you get started:
- simple tags: tags outputting a value that can (optionally) be assigned to a new variable
- inclusion tags: tags including and rendering other templates
- closable tags: tags involving closing statements and doing something with the output of a block
Simple tags
Simple tags usually output a value while allowing this value to be assigned to a new variable (that will be added to the template context). They can eventually take arguments in order to return the right result at rendering time.
Let's take the example of a local_time
template tag that outputs the string representation of the local time and that takes one mandatory argument (the format used to output the time). Such a template tag could be implemented as follows:
class LocalTimeTag < Marten::Template::Tag::Base
include Marten::Template::Tag::CanSplitSmartly
@assigned_to : String? = nil
def initialize(parser : Marten::Template::Parser, source : String)
parts = split_smartly(source)
if parts.size < 2
raise Marten::Template::Errors::InvalidSyntax.new(
"Malformed local_time tag: one argument must be provided"
)
end
@pattern_expression = Marten::Template::FilterExpression.new(parts[1])
# Identify possible assigned variable name.
if parts.size > 2 && parts[-2] == "as"
@assigned_to = parts[-1]
elsif parts.size > 2
raise Marten::Template::Errors::InvalidSyntax.new(
"Malformed local_time tag: only one argument must be provided"
)
end
end
def render(context : Marten::Template::Context) : String
time_pattern = @pattern_expression.resolve(context).to_s
local_time = Time.local(Marten.settings.time_zone).to_s(time_pattern)
if @assigned_to.nil?
local_time
else
context[@assigned_to.not_nil!] = local_time
""
end
end
end
As you can see template tags are initialized from a parser (instance of Marten::Template::Parser) and the raw "source" of the template tag (that is the content between the {%
and %}
tag delimiters). The #initialize
method is responsible for extracting any information that might be necessary to implement the template tag's logic. In the case of the local_time
template tag, we must take care of a few things:
- ensure that we have a format specified as argument (and raise an invalid syntax error otherwise)
- initialize a filter expression (instance of Marten::Template::FilterExpression) from the format argument: this is necessary because the argument can be a string literal or variable with filters applied to it
- verify if the output of the template tag is assigned to a variable by looking for an
as
statement: if that's the case the name of the variable is persisted in a dedicated instance variable
The #render
method is called at rendering time: it takes the current context object as argument and must return a string. In the above example, this method "resolves" the time format expression that was identified at initialization time from the context (which is necessary if it was a variable) and generates the right time representation. If the tag wasn't specified with an as
variable, then this value is simply returned, otherwise, it is persisted in the context and an empty string is returned.
Inclusion tags
Inclusion tags are similar to simple tags: they can take arguments (mandatory or not), and assign their outputs to variables, but the difference is that they render a template in order to produce the final output.
Let's take the example of a list
template tag that outputs the elements of an array in a regular ul
HTML tag. The template being rendered by such template tag could look like this:
<ul>
{% for item in list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
And the template tag itself could be implemented as follows:
class ListTag < Marten::Template::Tag::Base
include Marten::Template::Tag::CanSplitSmartly
@assigned_to : String? = nil
def initialize(parser : Marten::Template::Parser, source : String)
parts = split_smartly(source)
if parts.size < 2
raise Marten::Template::Errors::InvalidSyntax.new(
"Malformed list tag: one argument must be provided"
)
end
@list_expression = Marten::Template::FilterExpression.new(parts[1])
# Identify possible assigned variable name.
if parts.size > 2 && parts[-2] == "as"
@assigned_to = parts[-1]
elsif parts.size > 2
raise Marten::Template::Errors::InvalidSyntax.new(
"Malformed list tag: only one argument must be provided"
)
end
end
def render(context : Marten::Template::Context) : String
template = Marten.templates.get_template("path/to/list_tag.html")
rendered = ""
context.stack do |include_context|
include_context["list"] = @list_expression.resolve(context)
rendered = template.render(include_context)
end
if @assigned_to.nil?
Marten::Template::SafeString.new(rendered)
else
context[@assigned_to.not_nil!] = rendered
""
end
end
end
As you can see, the implementation of this tag looks quite similar to the one highlighted in Simple tags. The only differences that are worth noting here are:
- the argument of the template tag corresponds to the list of items that should be rendered
- the
#render
method explicitly renders the template mentioned previously by using a context with the "list" object in it (the#stack
method allows to create a new context where new values are stacked over the existing ones). The output of this rendering operation is either assigned to a variable or returned directly depending on whether theas
statement was used
Closable tags
Closable tags involve a closing statement, like this is the case for the {% block %}...{% endblock %}
template tag for example. Usually, such tags will "capture" all the nodes between the opening tag and the closing tag, render them at rendering time, and do something with the output of this rendering.
To illustrate this, let's take the example of a spaceless
tag that will remove whitespaces, tabs and new lines between HTML tags. Such a template tag could be implemented as follows:
class SpacelessTag < Marten::Template::Base
@inner_nodes : Marten::Template::NodeSet
def initialize(parser : Marten::Template::Parser, source : String)
@inner_nodes = parser.parse(up_to: %w(endspaceless))
parser.shift_token
end
def render(context : Marten::Template::Context) : String
@inner_nodes.render(context).strip.gsub(/>\s+</, "><")
end
end
In this example, the #initialize
method explicitly calls the parser's #parse
in order to parse the following "nodes" up to the expected closing tag (endspaceless
in this case). If the specified closing tag is not encountered, the parser will automatically raise a syntax error. The obtained nodes are returned as a "node set" (instance of Marten::Template::NodeSet
): this is a special object returned by the template parser that maps to multiple parsed nodes (those can be tags, variables, or plain text values) that can be rendered through a #render
method at rendering time.
The #render
method of the above tag is relatively simple: it simply "renders" the node set corresponding to the template nodes that were extracted between the {% spaceless %}...{% endspaceless %}
tags and then removes any whitespaces between the HTML tags in the output.
Registering template tags
In order to be able to use custom template tags, you must register them to Marten's global template tags registry.
To do so, you will have to call the Marten::Template::Tag#register
method with the name of the tag you wish to use in templates, and the template tag class.
For example:
Marten::Template::Tag.register("local_time", LocalTimeTag)
With the above registration, you could technically use this tag (the one from the above Simple tags section) as follows:
{% local_time "%Y-%m-%d %H:%M:%S %:z" %}