Skip to main content

Handlebars Templates

The preferred way to write the renderers for UI5 Web Components (and supported directly by the build tools) is to use standard Handlebars templates with some additional custom syntax.

Handlebars compilation​

Handlebars templates (.hbs) are compiled during build/development to lit-html templates (.lit.js) and the lit templates are what's actually executed during runtime.

Example:

The following src/MyComponent.hbs template

<button>{{text}}</button>

will be compiled to dist/generated/templates/MyComponentTemplate.lit.js with the following content,

import { html, svg, repeat, classMap, styleMap, ifDefined, unsafeHTML, scopeTag } from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
const block0 = (context, tags, suffix) => html`<button>${ifDefined(context.text)}</button>`;
export default block0;

and later tree-shaken by the bundler and bundled along with the rest of the component's code.

Therefore, the .hbs file is there just for convenience, the end result will always be an optimized lit-html.

Design goals of the Handlebars templates​

  • Declarative: write HTML in a form as close as possible to what will eventually be in the DOM (rather than writing template functions directly).
  • Abstract: the template could be compiled to other formats in the future (not just lit-html) so it should only use universal concepts and no lit-specific features.
  • Separation of concerns: the template must be as simple as possible with no complex expressions or calculations - variables that control structures (for example, {{#if}} statements) should be precalculated.

For these reasons, we would suggest you use .hbs templates and have them compiled to lit-html, instead of directly writing lit-html renderers, although that's also possible if you prefer so.

The Context​

Global context​

The context in the .hbs file is the web component instance, and you do not have to write the this keyword (although you can). Therefore, you can directly use metadata entities (property, slot, event names) or any other JavaScript property on the component directly:

In the MyComponent.js file:

this.age = 30;
this.fullName = `${this.name} ${this.lastName}`;

In the MyComponent.hbs file you can just use them directly:

<p>{{fullName}}</p>
<p>{{age}}</p>

The following code will have exactly the same result:

<p>{{this.fullName}}</p>
<p>{{this.age}}</p>

but this is optional, so it's almost never used.

Context in loops​

In a loop, the context is always the current item, and not the component itself.

Example:

In the MyComponent.js file:

this.items = [
{
id: "item1",
posinset: 1,
setsize: 5,
text: "Item 1"
},
{
id: "item2",
posinset: 2,
setsize: 5,
text: "Item 2"
}
]

In the MyComponent.hbs file:

{{#each items}}
<div id="{{id}}"
role="option"
aria-posinset="{{posinset}}"
aria-setsize="{{setsize}}"
>{{text}}</div>
{{/each}}

Again, you can use the this keyword, but it's not necessary. The following code will be the same as the one above:

{{#each items}}
<div id="{{this.id}}"
role="option"
aria-posinset="{{this.posinset}}"
aria-setsize="{{this.setsize}}"
>{{this.text}}</div>
{{/each}}

The only use case where you must use the this keyword is when you want to refer to the looped over item directly (and not its properties).

Example:

{{#each items}}
<div id="{{id}}"
.item="{{this}}"
>{{text}}</div>
{{/each}}

Here, each div inside the loop gets assigned an item property that points to the respective item from the array we're looping over.

Here's another example for the this keyword:

In the MyComponent.js file:

this.numbers = [
[1, 2, 3],
[4, 5, 6]
];

In the MyComponent.hbs file:

{{#each numbers}}
<div>
{{#each this}}
<span>{{this}}</span>
{{/each}}
</div>
{{/each}}

The result in the DOM would be:

<div><span>1</span><span>2</span><span>3</span></div>
<div><span>4</span><span>5</span><span>6</span></div>

In this example, the first usage of this (in the nested #each) is the nested array (for example, [1, 2, 3]), and the second usage of this inside the span is the number itself.

Accessing the global context from loops ​

You can access the global context inside loops with the "one-level-up" expression: ../

Example:

In the MyComponent.js file:

this.name = "John Smith";
this.items = [
{
id: "item1"
},
{
id: "item2"
}
]

In the MyComponent.hbs file:

{{#each items}}
<div id="{{id}}">{{../name}}</div>
{{/each}}

In this example, even though we're looping over an item from the array, we can still access the global context and use the name property of the web component instance.

The .hbs Syntax​

You can use the following features when writing .hbs templates:

Bindings​

You can access any property from the context (generally the web component instance) in your .hbs template with {{ and }}.

In the MyComponent.js file:

this.tooltip = "Some tooltip";
this.txt = "Some text";

In the MyComponent.hbs file:

<button title="{{tooltip}}">{{txt}}<button/>

Note: You must always create valid HTML, so you can only use bindings for attribute values or text nodes.

For example, the following is not allowed:

<{{tag}} {{attr}}="Hello">This will not compile</{{tag}}>

You can access object properties:

In the MyComponent.js file:

this.person = {
name: "John",
lastName: "Smith"
}

In the MyComponent.hbs file:

<p>{{person.name}} {{person.lastName}}</p>

but you cannot use expressions inside .hbs templates. The following is not allowed:

<p>{{person.name + " " + person.lastName}}</p>

Instead, you should precalculate the required value in the .js file and use it directly in the template:

In the MyComponent.js file:

get fullName() {
return `${this.person.name} ${this.person.lastName}`;
}

In the MyComponent.hbs file:

<p>{{fullName}}</p>

By default, all content that you pass is escaped for security purposes. However, you can pass arbitrary HTML with {{{ and }}}:

In the MyComponent.js file:

this.unsafeMessage = `<span>This is unsafe content</span>`;

In the MyComponent.hbs file:

<p>{{{unsafeMessage}}}</p>

The result in DOM would be:

<p><span>This is unsafe content</span></p>

Note: Using {{{ and }}} is strongly discouraged and should be avoided whenever possible. If you must use it, make sure you've sanitized your HTML manually beforehand. A common use-case for the {{{ and }}} binding is to manually add <strong> tags to parts of a string to implement highlighting while the user is typing. Here's an example:

In the MyComponent.js file:

this.userInput = `<strong>Arg</strong>entina`;

In the MyComponent.hbs file:

<div>{{{userInput}}}</div>

Thus, if the user has typed "Arg" (while typing "Argentina"), this part of the name will be highlighted.

Finally, it is possible to pass HTML elements (not just strings as in all examples above), and they will be rendered:

In the MyComponent.js file:

this.messageDiv = document.createElement("div");
this.messageDiv.textContent = "Hello";

In the MyComponent.hbs file:

<p>{{messageDiv}}</p>

The result in DOM would be:

<p><div>Hello</div></p>

Note: This is not to be confused with {{{ and }}}. The {{{ and }}} binding expects a string, containing HTML, while the example above demonstrates passing an HTML element (hence Object, not String) directly.

Note: Although this technique is allowed and has its uses (such as cloning slotted elements to another component), passing HTML directly is strongly discouraged. The best practice is to always write your HTML explicitly in the template.

Conditions​

You can use if, else and unless to create conditions.

Examples:

{{#if hasText}}
<label class="ui5-badge-text"><bdi><slot></slot></bdi></label>
{{/if}}

or

{{#if hasText}}
<label class="has-text"><span>{{text}}</span></label>
{{else}}
<label class="empty-label"></label>
{{/if}}

or

{{#unless _isPhone}}
<p>Some content</p>
{{/unless}}

You can chain if-else-if, as follows:

{{#if hasImage}}
<slot></slot>
{{else if icon}}
<ui5-icon class="ui5-avatar-icon" name="{{icon}}" accessible-name="{{accessibleNameText}}"></ui5-icon>
{{else if initials}}
<span class="ui5-avatar-initials">{{validInitials}}</span>
{{/if}}

Again, you cannot use expressions, so the following is not allowed:

{{#if person.access === "admin" }}
<p>Show admin functionality</p>
{{/if}}

Instead, you should have a precalculated value in your .js file, for example:

In MyComponent.js:

get isAdmin() {
return this.person.access === "admin";
}

and then use this value in MyComponent.hbs:

{{#if isAdmin }}
<p>Show admin functionality</p>
{{/if}}

Loops​

You can use each to loop over arrays.

In the MyComponent.js file:

this.items = [
{
id: "item1",
posinset: 1,
setsize: 5,
text: "Item 1"
},
{
id: "item2",
posinset: 2,
setsize: 5,
text: "Item 2"
}
]

In the MyComponent.hbs file:

{{#each items}}
<div id="{{id}}"
role="option"
aria-posinset="{{posinset}}"
aria-setsize="{{setsize}}"
>{{text}}</div>
{{/each}}

See the previous section (especially the Context in loops part) for more examples and the meaning of the this keyword in loops.

You can access the index of the currently looped item with the special {{@index}} variable. Note that {{@index}} is zero-based.

For example, the following template,

{{#each items}}
<div id="{{id}}"
part="item-{{@index}}"
>{{text}}</div>
{{/each}}

will produce:

<div id="item1" part="item-0"></div>
<div id="item2" part="item-1"></div>

This is a common technique to create unique shadow parts for items within a UI5 Web Component.

Property assignment (the . prefix)​

The . prefix allows you to bind by property, rather than by attribute.

Consider the following example:

this.id = "myId";
this.someString = "Some data";
this.item = {
a: 1,
b: 2
};
this.text = "Some text";
<div
id="{{id}}"
data-info="{{someString}}"
.item="{{myItem}}"
>
{{text}}
</div>

While data-info is set as an attribute (default assignment), item is set as a property due to the . used. The result in the DOM would be:

<div id="myId" data-info="Some data">Some text</div>

There would be no item in the DOM at all, but the following code:

document.getElementById("myId").item

would return the item object because it was set as a property.

Boolean attribute assignment (the ? prefix) ​

The ? prefix signifies that an attribute must not be set in DOM at all, if the bound value is falsy.

Consider the following example:

this._id = "myCB";
this.checked = false;
this.readonly = false;
this.disabled = false;
<input
id="{{_id}}-CB"
type='checkbox'
?checked="{{checked}}"
?readonly="{{readonly}}"
?disabled="{{disabled}}"
tabindex="-1"
aria-hidden="true"
data-sap-no-tab-ref
/>

Since the checked, readonly, and disabled attributes are all Boolean, they must not be in the DOM if we want the <input> to be interactive.

The output in DOM would be:

<input
id="myCB-CB"
type='checkbox'
tabindex="-1"
aria-hidden="true"
data-sap-no-tab-ref
/>

All attributes that had the ? prefix and were bound to a falsy value are gone from DOM.

However, if you did not use the ? prefix

<input
id="{{_id}}-CB"
type='checkbox'
checked="{{checked}}"
readonly="{{readonly}}"
disabled="{{disabled}}"
tabindex="-1"
aria-hidden="true"
data-sap-no-tab-ref
/>

even though checked, readonly, and disabled are equal to false, the resulting DOM would be

<input
id="myCB-CB"
type='checkbox'
checked=""
readonly=""
disabled=""
tabindex="-1"
aria-hidden="true"
data-sap-no-tab-ref
/>

which is not what we want, since boolean HTML attributes don't need to have a value at all to be considered set, only their presence is required. Therefore, always bind boolean attributes with ?.

Event handlers assignment (the @ prefix) ​

You can bind events as follows:

In the MyComponent.js file:

this.onClick = event => {};

In the MyComponent.hbs file:

<button @click="{{onClick}}"></button>

Style maps​

Style maps are an easy and useful tool to apply multiple styles to an element dynamically.

In order to use a style map in your .hbs template, you must bind a styles property (or as in the next example, a getter called styles). Any binding to a styles object on a style attribute will be treated as a style map.

In the MyComponent.js file:

get styles() {
return {
root: {
display: this.isBlock ? "block" : "inline",
width: `${this.x}px`,
height: `${this.y}px`
},
footer: {
backgroundColor: this.bgColor
}
}
}

In the MyComponent.hbs file:

<div style="{{styles.root}}">
Some content
<footer style="{{styles.footer}}"></footer>
</div>

After the following code is run, both the div and the footer will have the respective CSS styles applied to them.

Important: do not build styles manually. Always use style maps as they are CSP-compliant and they will not build style strings and assign them, but will use JavaScript APIs to apply each style/CSS variable separately.

The following is an anti-pattern and is not allowed in the latest version of the handlebars-to-lit compiler:

this.display = "block";
this.styles = "display: none; visibility: hidden";
<div style="display: {{display}}"></div>
<div style="{{styles}}"></div>

In the first example, we build a style value manually, and in the second example we pass hard-coded styles as a string. None of these are CSP-compliant. The correct way would be to pass objects (as in the first example), in which case a style map will be used.

Class maps​

Class maps are an easy tool to set multiple classes to an element - either conditionally, or unconditionally.

In order to use a class map in your .hbs template, you must bind a classes property (or as in the next example, a getter called classes) to a class attribute:

get classes() {
return {
main: {
"ui5-myComponent-main": true,
"ui5-myComponent-mobile": isPhone()
},
content :{
"ui5-content-wide": this.width > 1024
},
section: {
"ui5-section": true,
"ui5-section-with-items": this.items.length > 0,
"ui5-section-desktop": !isPhone() && !isTablet()
}
}
}
<article class="{{classes.main}}">
<div class="{{classes.content}}"></div>
<section class="{{classes.section}}"></section>
</article>

Here, all 3 HTML elements will have their classes applied based on the conditions in the definition of the class map. Some entries in the class map are unconditional (ui5-myComponent-main and ui5-section) so these classes will always be set, however the rest are going to be set only if certain criteria are met.

Partials ​

You can use partials to reuse code in .hbs templates:

You can define a partial with {{#*inline "NAME"}} and use it with {{>NAME}} where NAME is the name of the partial.

Consider the following example:

<div>
{{>valueStateMessage}}
</div>
<p>Some more content</p>
<div>
{{>valueStateMessage}}
</div>

{{#*inline "valueStateMessage"}}
{{#if msg}}
<span>{{msg}}</span>
{{/if}}
{{/inline}}

Here we define some common code in the valueStateMessage partial and use it twice within the template.

Partials are very often used to define hooks - extension points for other components.

Example:

In MyComponent.hbs:

<section>
<span class="first-fe" data-ui5-focus-trap tabindex="0" @focusin={{forwardToLast}}></span>

{{> beforeContent}}

<div style="{{styles.content}}" class="{{classes.content}}" @scroll="{{_scroll}}" part="content">
<slot></slot>
</div>

{{> afterContent}}

<span class="last-fe" data-ui5-focus-trap tabindex="0" @focusin={{forwardToFirst}}></span>
</section>


{{#*inline "beforeContent"}}{{/inline}}

{{#*inline "afterContent"}}{{/inline}}

Here we define two empty partials (beforeContent and afterContent) for others to implement.

Note: Partials do not have their own context. When a partial is processed, its content is treated as if directly written at the partial's insertion point.

Include Template​

You can include other .hbs files with {{>include "PATH_TO_FILE"}} where PATH_TO_FILE is a relative or absolute path to the .hbs file you want to include.

Example:

{{>include "./MyComponent.hbs"}}

Paths to .hbs files from other node_modules/ libraries are also supported.

Example:

{{>include "@ui5/webcomponents/src/Popup.hbs"}}

The most common use case for {{>include}} is to include an .hbs file that has extension points (hooks) and implement them. Given the example from the previous section (about Partials), consider the following:

In MyComponent2.hbs:

{{>include "./MyComponent.hbs"}}

{{#*inline "beforeContent"}}
<span>Implementation here</span>
{{/inline}}

{{#*inline "afterContent"}}
<span>Another implementation here</span>
{{/inline}}

Then the MyComponent2 component will use the .hbs file of the MyComponent component but with its own version of its partials.

Using the slot element​

Rendering slots​

The slot element allows you to render children, nested in your web component, in a desired place in the shadow DOM. You should render each slot, defined in your component (see the Slots) section, somewhere in the .hbs template.

  • To render the default slot simply render a slot tag:
<slot></slot>
  • To render a named slot:
<slot name="tabs"></slot>
  • Here's a real-world example of a "page" component:

In Page.js (metadata object):

@slot()
header!: Array<HTMLElement>;

@slot({ type: Node, "default": true })
content!: Array<Node>;

@slot()
footer!: Array<HTMLElement>;
}

In Page.hbs:

<div class="ui5-page-root">
<header class="ui5-page-header-root" id="ui5-page-header">
<slot name="header"></slot>
</header>

<section part="content" class="ui5-page-content-root" style="{{styles.content}}">
<slot></slot>
</section>

<footer class="ui5-page-footer-root" style="{{styles.footer}}">
<slot name="footer"></slot>
</footer>
</div>

We render 3 slot elements - a default slot (unnamed) and 2 named slots - respectively with name equal to header and footer.

All children, passed to the component with no slot attribute, will then be rendered by the browser where the default <slot></slot> is, and all children with attributes slot="header" / slot="footer" will be rendered where the respective named slot is.

Individual slots​

All children assigned to a certain slot, are rendered by the browser next to each other in the exact order in which they were passed to the component. Sometimes, however, each child must be placed separately in the shadow root, potentially wrapped in other HTML elements, to satisfy the UX design of the component.

The individualSlots slot metadata configuration setting (see the Slot section) allows you to have a separate physical slot for each child belonging to a certain slot.

However, setting individualSlots: true in the metadata configuration only creates an _individualSlot property on each element belonging to the slot, but does not create any slots automatically. The individual slots must be explicitly rendered by the developer in the .hbs template.

To do so, simply render a slot with a name property equal to the _individualSlot value for each child.

Here's an example:

In MyComponent.js (metadata object):

@slot({
type: HTMLElement,
"default": true,
individualSlots: true,
})
items!: Array<HTMLElement>;

Since propertyName is set to items, the children of the default slot will be accessible on the web component instance with this.items; and since individualSlots is set to true, every child in this.items (every child slotted in the default slot) will have an _individualSlots property created by the framework.

In MyComponent.hbs you must render a slot for each child with name equal to the _individualSlot property value for this child:

{{#each items}}
<div class="item-wrapper">
<slot name="{{_individualSlot}}"></slot>
</div>
{{/each}}

The resulting DOM from the loop above will look like this:

<div class="item-wrapper"><slot name="items-1"></slot></div>
<div class="item-wrapper"><slot name="items-2"></slot></div>
<div class="item-wrapper"><slot name="items-3"></slot></div>

This allows you to have arbitrary DOM around each child and implement complex UX design, otherwise impossible if all children were just normally rendered next to each other in a single slot.