Introduction to templates
Templates provide a convenient way for defining the presentation logic of a web application. They allow to write textual content that is rendered dynamically by using a dedicated syntax. This syntax enables the use of dynamic variables as well as some programming constructs.
Syntax
A template is a textual document or a string that makes use of the Marten template language, and that can be used to generate any text-based format (HTML, XML, etc). In order to insert dynamic content, templates usually make use of a few constructs such as variables, which are replaced by the corresponding values when the template is evaluated, and tags, which can be used to implement the logic of the template.
For example, the following template displays the properties of an article
variable and loops over the associated comments in order to display them as a list:
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
<ul>
{% for comment in article.comments %}
<li>{{ comment.message }}</li>
{% else %}
<li>No comments!</li>
{% endfor %}
</ul>
Templates need to be evaluated with a context. This context is usually a hash-like object containing all the variables or values that can be used by the template when it is rendered.
In the previous example, the template context would at least contain one article
key giving access to the considered article properties.
Variables
Variables can be used to inject a value from the context into the rendered template. They must be surrounded by {{
and }}
.
For example:
Hello, {{ name }}!
If the context used to render the above template is {"name" => "John Doe"}
, then the output would be "Hello, John Doe!".
Each variable can involve additional lookups in order to access specific object attributes (if such objects have ones). These lookups are expressed by relying on a dot notation (foo.bar
). For example, the following snippet would output the title
attribute of the article
variable:
<h1>{{ article.title }}</h1>
This notation can be used to call object methods but also to perform key lookups for hashes or named tuples. It can also be used to perform index lookups for indexable objects (such as arrays or tuples):
{{ my_array.0 }}
Filters
Filters can be applied to variables or tag arguments in order to transform their values. They are applied to these variables or arguments through the use of a pipe (|
) followed by the name of the filter.
For example, the following snippet will apply the capitalize
filter to the output of the name
variable, which will capitalize the value of this variable:
Hello, {{ name|capitalize }}!
It should be noted that some filters can take an argument. When this is the case, the argument is specified following a colon character (:
).
For example, the following snippet will apply the default
filter to the output of the name
variable in order to fallback to a default name if the variable has a null value:
Hello, {{ name|default:"Stranger" }}!
It should be noted that the fact that an argument is supported or not, and mandatory or not, varies based on the considered filter. In all cases, filters can support up to one argument only.
Please head over to the filters reference to see a list of all the available filters. Implementing custom filters is also a possibility that is documented in Create custom filters.
Tags
Tags allow to do method-calling and to run any kind of logic within a template. Some tags allow to perform control flows (like if conditions, or for loops) while others simply output values. They are delimited by {%
and %}
.
For example, the following snippet makes use of the assign
tag to create a new variable within a template:
{% assign my_var = "Hello World!" %}
As mentioned above, some tags allow to perform control flows and require a "closing" tag, like the for
or if
tags:
{% for article in articles %}
{{ article.title }} is {% if not article.published? %}not {% endif %}published
{% endfor %}
Some tags also require arguments. For example, the url
template tag requires at least the name of the route for which the URL resolution should be performed:
{% url "my_route" %}
Please head over to the tags reference to see a list of all the available template tags. Implementing custom tags is also a possibility that is documented in Create custom tags.
Comments
Comments can be inserted in any templates and must be surrounded by {#
and #}
:
{# This will not be evaluated #}
Template inheritance
Templates can inherit from each other: this allows you to easily define a "base" template containing the layout of your application so that you can reuse it in order to build other templates, which helps in keeping your codebase DRY.
This works as follows:
- a "base" template defines the shared layout as well as "blocks" where child templates will actually inject their own contents
- "child" templates "extend" from the base template and explicitly define the contents of the "blocks" that are expected by the base template
For example, a "base" template could look like this:
<html>
<head>
<title>{% block title %}My super website{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
Here the base template defines two blocks by using the block
template tag. Using this tag essentially makes it possible for any child templates to "override" the content of these blocks.
Note that it is possible to specify the name of the block being closed in the endblock
tag to improve readability. For example:
{% block title %}
My super website
{% endblock title %}
Given the above base template (that we assume is named base.html
), a "child" template making use of it could look like this:
{% extend "base.html" %}
{% block title %}Custom page title{% endblock %}
{% block content %}Custom page content{% endblock %}
Here we make use of the extend
template tag in order to indicate that we want to inherit from the base.html
template that we created previously. When Marten encounters this tag, it'll make sure that the targetted template is properly loaded before resuming the evaluation of the current template.
The {% extend %}
tag should always be called at the top of the file, before the actual content of the template. Inheritance won't work properly if that's not the case.
We also use block
tags to redefine the content of the blocks that were defined in the base.html
template. It should be noted that if a child template does not define the content of one of its parent's blocks, the default content of this block will be used instead (if there is one!).
You can use many levels of template inheritance if needed. Indeed, a child.html
template can very well extend a base_dashboard.html
template, which itself extends a base.html
template for example.
It should be noted that it is also possible to get the content of a block from a parent template by using the super
template tag. This can be useful in situations where blocks in a child template need to extend (add content) to a parent's block content instead of overwriting it.
For example, with the following snippet the output of the title
block would be "My super website - Example page":
{% extend "base.html" %}
{% block title %}{% super %} - Example page{% endblock %}
{% block content %}Custom page content{% endblock %}
It's important to remember that the super
template tag can only be used within block
tags.
Template loading
Templates can be loaded from specific locations within your codebase and from application folders. This is controlled by two main settings:
templates.app_dirs
is a boolean that indicates whether or not it should be possible to load templates that are provided by installed applications. Indeed, applications can define atemplates
folder at their root, and these templates will be discoverable by Marten if this setting is set totrue
templates.dirs
is an array of additional directories where templates should be looked for
Application templates are always enabled by default (templates.app_dirs = true
) for new Marten projects.
It is possible to programmatically load a template by name. To do so, you can use the #get_template
method that is provided by the Marten templates engine:
Marten.templates.get_template("foo/bar.html")
This will return a compiled Template
object that you can then render by using a specific context.
Rendering a template
You won't usually need to interact with the "low-level" API of the Marten template engine in order to render templates: most of the time you will render templates as part of handlers, which means that you will likely end up using the #render
shortcut or generic handlers that automatically render templates for you.
That being said, it is also possible to render any Template
object that you loaded by leveraging the #render
method. This method can be used either with a Marten context object, a hash, or a named tuple:
template = Marten.templates.get_template("foo/bar.html")
template.render(Marten::Template::Context{"foo" => "bar"})
template.render({"foo" => "bar"})
template.render({ foo: "bar" })
Using custom objects in contexts
Most objects that are provided by Marten (such as Model records, query sets, schemas, etc) can automatically be used as part of templates. If your project involves other custom classes, and if you would like to interact with such objects in your templates, then you will need to explicitly ensure that they include the Marten::Template::Object
module.
Crystal being a statically typed language, the Marten engine needs to know which types of objects it is dealing with in advance in order to know (i) what can go into template contexts and (ii) how to "resolve" object attributes when templates are rendered. It is not possible to simply expect any Object
object, hence why we need to make use of a shared Marten::Template::Object
module to account for all the classes whose objects should be usable as part of template contexts.
Let's take the example of a Point
class that provides access to an x-coordinate and a y-coordinate:
class Point
getter x
getter y
def initialize(@x : Int32, @y : Int32)
end
end
By default, Point
objects cannot be used as part of templates. Let's say we want to render the following template involving a point
variable:
My point is: {{ point.x }}, {{ point.y }}
If you try to render such a template while passing a Point
object into the template context, you will encounter a Marten::Template::Errors::UnsupportedValue
exception stating:
Unable to initialize template values from Point objects
To remediate this, you will have to include the Marten::Template::Object
module in the Point
class and define a #resolve_template_attribute
method as follows:
class Point
include Marten::Template::Object
getter x
getter y
def initialize(@x : Int32, @y : Int32)
end
def resolve_template_attribute(key : String)
case key
when "x"
x
when "y"
y
end
end
end
Each class including the Marten::Template::Object
module must also implement a #resolve_template_attribute
method in order to allow resolutions of object attributes when templates are rendered (for example {{ point.x }}
). That being said, there are a few shortcuts that can be used in order to avoid writing such methods.
The first one is to use the #template_attributes
macro in order to easily define the names of the methods that should be made available to the template runtime. For example, such macro could be used like this with our Point
class:
class Point
include Marten::Template::Object
getter x
getter y
def initialize(@x : Int32, @y : Int32)
end
template_attributes :x, :y
end
Another possibility is to include the Marten::Template::Object::Auto
module instead of the Marten::Template::Object
one in your class. This module will automatically ensure that every "attribute-like" public method that is defined in the including class can also be accessed in templates when performing variable lookups.
class Point
include Marten::Template::Object::Auto
getter x
getter y
def initialize(@x : Int32, @y : Int32)
end
end
Note that all "attribute-like" public methods will be made available to the template runtime when using the Marten::Template::Object::Auto
module. This may be a good enough behavior, but if you want to have more control over what can be accessed in templates or not, you will likely end up using Marten::Template::Object
and the #template_attributes
macro instead.
Using context producers
Context producers are helpers that ensure that common variables are automatically inserted in the template context whenever a template is rendered. They are applied every time a new template context is generated.
For example, they can be used to insert the current HTTP request object in every template context being rendered in the context of a handler and HTTP request. This makes sense considering that the HTTP request object is a common object that is likely to be used by multiple templates in your project: that way there is no need to explicitly "insert" it in the context every time you render a template. This specific capability is provided by the Marten::Template::ContextProducer::Request
context producer, which inserts a request
object into every template context.
Template context producers can be configured through the use of the templates.context_producers
setting. When generating a new project by using the marten new
command, the following context producers will be automatically configured:
config.templates.context_producers = [
Marten::Template::ContextProducer::Request,
Marten::Template::ContextProducer::Flash,
Marten::Template::ContextProducer::Debug,
Marten::Template::ContextProducer::I18n,
]
Each context producer in this array will be applied in order when a new template context is created and will contribute "common" context values to it. This means that the order of these is important since context producers can technically overwrite the values that were added by previous context producers.
Please head over to the context producers reference to see a list of all the available context producers. Implementing custom context producers is also a possibility that is documented in Create custom context producers.
Auto-escaping
The output of template variables is automatically escaped by Marten in order to prevent Cross-Site Scripting (XSS) vulnerabilities.
For example, let's consider the following snippet:
Hello, {{ name }}!
If this template is rendered with <script>alert('popup')</script>
as the content of the name
variable, then the output will be:
Hello, <script>alert('popup')</script>!
It should be noted that this behavior can be disabled explicitly. Indeed, sometimes it is expected that some template variables will contain trusted HTML content that you intend to embed into the template's HTML.
To do this, it is possible to make use of the safe
template filter. This filter "marks" the output of a variable as safe, which ensures that its content is not escaped before being inserted in the final output of a rendered template.
For example:
Hello, {{ name }}!
Hello, {{ name|safe }}!
When rendered with <b>John</b>
as the content of the name
variable, the above template will output:
Hello, <b>John</b>!
Hello, <b>John</b>!