HTML Template Language (also referred to as HTL) is Adobe Experience Manager’s preferred and recommended server-side template system for HTML.
There is a difference between HTL and other template languages that makes it tricky to properly support all features, or to transpile into the cleanest code.
HTL itself is standard HTML5, and template directives are placed as HTML tags and attributes. Handlebars is a string-based template engine, where control structures can be placed anywhere in the document. If we were to literally transpile everything in place, it would end up as invalid HTML. This is why there are some restrictions in usage of certain block tags in certain places.
To better work with those restrictions, we'll introduce Handlebars Helpers that can be transpiled the proper way.
An example:
<!-- hbs -->
<div {{#if foo }}class="{{foo}}" {{/if}}>
foobar
</div>
In the above template, the if
block has no context, so it doesn't know it's in the middle of an
HTML element. Replacing that with <sli data-sly-test="${ foo }">
within the <div
would result
in invalid HTML.
The proper outcome would be:
<div data-sly-attribute.class="${ foo }">
foobar
</div>
To make the translation better, we could introduce a handlebars helper to do the following:
<div {{#attr class foo }}>
foobar
</div>
It would still only allow if
blocks outside of HTML tags, but it allows for a way to properly deal
with those scenarios.
In Handlebars and other template languages, you have an else
block that is rendered if if
expression is false. Additionally there is support for else if
chaining.
In HTL there is no such concept. However, it allows you to store the result of the if
statement
in a variable, and use it in a later if
. If you negate that variable, you get the inverse, and
thus an else
.
The transpiler will auto-generate those variables for all if-statements, and use them in else
and
else if
expressions. Additionally, you can name them in Handlebars by passing additional
parameters to the if
block (that are ignored by the runtime).
The following example displays how the result of the if
is used to create an else.
{{if foo}}
Foo
{{else}}
Bar
{{/if}}
<sly data-sly-test.result1="${ foo }">
Foo
</sly>
<sly data-sly-test="${ !(result1) }">
Bar
</sly>
The example below shows how to name your resulting variable, and how multiple if/elseif
cases are
used in the else
. Notice the as isFoo
.
{{if foo as isFoo}}
Foo
{{else if bar}}
Bar
{{else}}
Baz
{{/if}}
<sly data-sly-test.isFoo="${ foo }">
Foo
</sly>
<sly data-sly-test.result1="${ !(isFoo) || bar }">
Bar
</sly>
<sly data-sly-test="${ !(isFoo || result1) }">
Baz
</sly>
Handlebars only has a single concept of partials. You can include af file (either statically or dynamically) and optionally pass parameters.
In HTL you have two ways to include a partial, either by data-sly-include
or by data-sly-call
.
The first can include any HTL template file and executes it in isolation, there is no way to pass variables to the template file.
The second requires that you first define a template with the variables you want to pass, then use
that template and call
it with the variables you want to pass.
Since there is no way of telling how to include a Handlebars partial (in HTL you can call a template without passing variables, which in Handlebars would look the same as including a template file), and there is no way to tell how a template should be registered (either as normal file, or in a template tag), we need to add some extra metadata/rules for this to work.
In Muban there is a concept of blocks
vs components
, where blocks can be rendered from code and
directly included on a page, and components can only be included within blocks or other components.
Following this division, we could mark blocks as normal template files, and components as template
tags. By disallowing you to include blocks in blocks, we should always use the call
method.
In HTL is preferred to use data-sly-*
attributes on existing HTML elements to add logic. In
Handlebars there is no such option; all logic is added around the HTML.
Luckily there is a sly
tag in HTL that allows you to add logic to the DOM without introducing
new DOM elements. So this is the obvious target of all control-logic. The downside is that the
generated templates don't look as good as you would write them yourself.
There are valid reasons to use the sly
tag though, for example when having to repeat two DOM
elements, or conditionally hide two DOM elements without having to duplicate the data-sly-test
on both elements.
However, when dealing with single elements, those attributes could be moved to the child or parent DOM element to deliver cleaner templates. To detect this, one would need to create an actual DOM and detect if the control element is a single child or contains only a single child. Since the source template is no valid HTML, this is not possible.
Other options that work with the raw string are error-prone, since there is no way to know if a tag is displayed as HTML, or as a comment or attribute value. One could create a custom parser, or try to make guesses from the HBS AST, maybe this can be implemented in the future.
The following Handlebars to HTL conversions are currently supported:
hbs input:
<div class="entry">
{{!-- comment 1 --}}
{{! comment 2 }}
inline {{! comment 3 }} test
<!-- html comment -->
</div>
htl output:
<div class="entry">
<!--/* comment 1 */-->
<!--/* comment 2 */-->
inline <!--/* comment 3 */--> test
<!-- html comment -->
</div>
hbs input:
<h1>{{ foo }}</h1>
<h1>{{ foo.bar }}</h1>
htl output:
<h1>${ foo }</h1>
<h1>${ foo.bar }</h1>
hbs input:
{{#each users }}
{{ name }}
{{#each comments }}
{{ ../name }} {{title}}
{{#each commenter }}
{{../../name}} {{name}}
{{/each}}
{{/each}}
{{/each}}
htl output:
<sly data-sly-list.users_i="${ users }">
${ users_i.name }
<sly data-sly-list.comments_i="${ users_i.comments }">
${ users_i.name } ${ comments_i.title }
<sly data-sly-list.commenter_i="${ comments_i.commenter }">
${ users_i.name } ${ commenter_i.name }
</sly>
</sly>
</sly>
hbs input:
{{ escaped }}
{{{ notEscaped }}}
htl output:
${ escaped }
${ notEscaped @ context='html' }
hbs input:
{{#if foo }}
Foo
{{/if}}
htl output:
<sly data-sly-test="${ foo }">
Foo
</sly>
hbs input:
{{#if foo as isFoo }}
Foo
{{else}}
Bar
{{/if}}
htl output:
<sly data-sly-test.isFoo="${ foo }">
Foo
</sly>
<sly data-sly-test="${ !(isFoo) }">
Bar
</sly>
hbs input:
{{#if foo }}
Foo
{{else if bar }}
Bar
{{/if}}
htl output:
<sly data-sly-test.result1="${ foo }">
Foo
</sly>
<sly data-sly-test="${ !(result1) && bar }">
Bar
</sly>
hbs input:
{{#if foo }}
Foo
{{else}}
{{# if bar }}
Bar
{{/if}}
Baz
{{/if}}
htl output:
<sly data-sly-test.result1="${ foo }">
Foo
</sly>
<sly data-sly-test="${ !(result1) }">
<sly data-sly-test="${ bar }">
Bar
</sly>
Baz
</sly>
hbs input:
{{#if foo }}
Foo
{{else if bar }}
Bar
{{else}}
Foobar
{{/if}}
htl output:
<sly data-sly-test.result1="${ foo }">
Foo
</sly>
<sly data-sly-test.result2="${ !(result1) && bar }">
Bar
</sly>
<sly data-sly-test="${ !(result1 || result2) }">
Foobar
</sly>
hbs input:
{{#if foo }}
Foo
{{else if bar as isBar }}
Bar
{{#if bb }}
bb
{{/if}}
{{else if baz}}
Baz
{{else}}
Foobar
{{/if}}
htl output:
<sly data-sly-test.result1="${ foo }">
Foo
</sly>
<sly data-sly-test.isBar="${ !(result1) && bar }">
Bar
<sly data-sly-test="${ bb }">
bb
</sly>
</sly>
<sly data-sly-test.result2="${ !(result1 || isBar) && baz }">
Baz
</sly>
<sly data-sly-test="${ !(result1 || isBar || result2) }">
Foobar
</sly>
hbs input:
{{#each users }}
<a href="{{ foobar }}">link</a>
{{/each}}
htl output:
<sly data-sly-list.users_i="${ users }">
<a href="${ users_i.foobar }">link</a>
</sly>
hbs input:
{{#each users }}
<a href="{{ foobar }}">{{ this }}</a>
{{/each}}
htl output:
<sly data-sly-list.users_i="${ users }">
<a href="${ users_i.foobar }">${ users_i }</a>
</sly>
hbs input:
{{#each users }}
{{ @index }}: {{ this }}
{{/each}}
htl output:
<sly data-sly-list.users_i="${ users }">
${ users_iList.index }: ${ users_i }
</sly>
hbs input:
{{#each map as |value key| }}
{{ key }}: {{ value }}
{{/each}}
htl output:
<sly data-sly-list.key="${ map }">
${ key }: ${ map[key] }
</sly>
hbs input:
{{#each users as |user| }}
{{ user.name }}
{{#each comments as |comment| }}
{{ user.name }} {{comment.title}}
{{/each}}
{{/each}}
htl output:
<sly data-sly-list.user="${ users }">
${ user.name }
<sly data-sly-list.comment="${ user.comments }">
${ user.name } ${ comment.title }
</sly>
</sly>
hbs input:
{{#each users }}
{{#if foo }}
{{#each comments }}
{{#if title}}
<a href="{{ id }}">{{ name }}</a>
{{/if}}
{{/each}}
{{else}}
{{#each comments }}
{{id}}
{{/each}}
{{/if}}
{{/each}}
htl output:
<sly data-sly-list.users_i="${ users }">
<sly data-sly-test.result1="${ users_i.foo }">
<sly data-sly-list.comments_i="${ users_i.comments }">
<sly data-sly-test="${ comments_i.title }">
<a href="${ comments_i.id }">${ comments_i.name }</a>
</sly>
</sly>
</sly>
<sly data-sly-test="${ !(result1) }">
<sly data-sly-list.comments_i="${ users_i.comments }">
${ comments_i.id }
</sly>
</sly>
</sly>
hbs input:
{{#each users }}
<a href="{{ id }}">{{ name }}</a>
{{#each comments }}
{{ name }}
{{/each}}
{{/each}}
htl output:
<sly data-sly-list.users_i="${ users }">
<a href="${ users_i.id }">${ users_i.name }</a>
<sly data-sly-list.comments_i="${ users_i.comments }">
${ comments_i.name }
</sly>
</sly>
hbs input:
{{> foo/bar.hbs }}
htl output:
<sly data-sly-use.lib="foo/bar.html" data-sly-call="${ lib.bar }" />
hbs input:
{{> (lookup . 'myVariable') }}
htl output:
<sly data-sly-use.lib="${ myVariable }" data-sly-call="${ lib[myVariable] }" />
hbs input:
{{> myPartial name=firstName age=18 foo="bar" baz=true }}
htl output:
<sly data-sly-use.lib="myPartial.html" data-sly-call="${ lib.myPartial @ name=firstName, age=18, foo='bar', baz=true }" />