A template is a JSON-like object that describes how to create and update a heirarchy of DOM elements.
Most of the documentation on templates assumes their use with Components. For information on using the template compiler directly see Template Internals.
Components declare their template as a static field
named template
:
class MyComponent extends Component
{
static template = {}; The template must be declared static
}
The structure of a template is a tree of "nodes" that describe a heirarchy of DOM elements that the template creates and manages.
There are different kinds of template nodes:
Plain Text - declared as a string or a function that returns a string:
// string
"Hello World, I'm a text node",
// callback
() => new Date().toString(),
HTML Text - declared by using the html()
directive to wrap a string or
a callback that returns a string.
// HTML string
html("<span>My Text Span</span>"),
// callback
html(() => `Now: <span>${new Date().toString()}</span>`),
HTML Elements - declared as an object with a type
property that is
the tag name of the element, attributes declared as other properties and
child nodes declared with the $
property:
{
type: "div", tag name
id: "my-div", id attribute
$: [ child nodes array
{ type: "span" }, child node 1
{ type: "span" }, child node 2
]
}
produces:
<div id="my-div">
<span></span>
<span></span>
</div>
Fragments - fragments are multi-root elements declared as an object with child nodes, but no type property:
{ no "type" attribute makes this a fragment
$: [
"child 1",
"child 2",
"child 3",
]
}
Components - templates can reference other components by declaring a node
with a type
property that is the component class and other properties
declaring properties to be assigned to tbe component.
{
type: MyComponent, the component class
myProp: "Hello World", will set "myProp" on the component
}
The $
property is called the content property.
For HTML elements it's an array of child nodes:
static template = {
type: "div .child-nodes-demo",
$: [
{
type: "span",
text: "apples",
},
{
type: "span",
text: "pears",
}
]
}
The above is equivalent to:
<div class="child-nodes-demo">
<span>apples</span>
<span>pears</span>
</div
You might look at the above template and HTML and feel the HTML is easier to type and clearer to understand.
For this simple example it is but by the time you add in dynamic properties, event handlers, component references, conditional blocks and list rendering the template format starts to become much more attractive (at least we think so).
If a node has only a single child node, there's no need for the array syntax:
$: { only a single child node, so no need for an array here
type: "span",
text: "apples",
},
Most values in a template can be declared dynamically by providing a callback instead of a static value:
{
type: "div",
text: c => c.divText() Call a function to get the text
}
The arguments passed to a callback are a model
object and a context
object.
callback(model, context)
When using templates with components the model
object is the component instance
which is why we conventionally call it c
. The context
object is often unused.
When any dynamic content changes, the template's update
method
needs to be called in order for the change to be reflected in the DOM.
When working with components this can be managed using the Component.invalidate()
or Component.update()
methods - see here.
This example toggles the text displayed in a div
each time it's clicked:
class MyComponent extends Component
{
#on = false;
// A dynamic property used by the template
get text()
{
return this.#on ? "On" : "Off";
}
onClick()
{
this.#on = !this.#on;
this.invalidate(); Tells component to update
}
static template = {
type: "div",
text: c => c.text, Callback for dynamic content
on_click: "onClick", See below for more on event handlers
}
}
To listen to events raised by HTML elements and components,
add a property to the template node using the event named prefixed
with on_
.
on_click: c => c.onClick();
This example listens to a button for its "click" event:
class MyButton extends Component
{
onClick()
{
alert("Oi!");
}
static template = {
type: "button",
text: "Click Me",
on_click: c => c.onClick(), event handler
}
}
If you need the event object, it's passed as the second parameter to the callback:
on_click: (c,ev) => c.onClick(ev);
This example uses preventDefault()
to prevent navigation when clicking
on an <a>
element:
class MyComponent extends Component
{
onClick(ev)
{
ev.preventDefault();
alert("Navigation Cancelled");
}
static template = {
type: "a",
href: "https://codeonlyjs.org/",
text: "Link",
on_click: (c, ev) => c.onClick(ev), event handler
}
}
Instead of passing a function as the event handler, you can instead pass the string name of a function to call on the component.
The following is identical to the previous example, passing (ev)
to
the handler:
on_click: "onClick",
To access the DOM nodes and nested components constructed by a template
use the bind
directive to specify the name of a property on your component.
When the DOM tree is created, the specified property will be assigned a reference to the created element (or component).
For example this would make the input field available to the component
as this.myInput
:
{
type: "input",
bind: "myInput",
}
Before accessing bound fields, you need to make sure the component has been "created" and not just constructed. Normally a component's DOM tree isn't created until it's first mounted.
If you need the DOM elements beforehand, call the component's create()
method to ensure the DOM has been created.
For example, suppose you're using a third party light-box component as a photo viewer and it needs to be passed a root element to work with.
export class MyLightBox extends Component
{
constructor()
{
super();
// Ensure DOM created before accessing bound elements
this.create();
// `this.lightbox` will be set to the div element
externalLightBoxLibrary.init(this.lightbox);
}
static template = [
{
type: "div",
bind: "lightbox", Causes this.lightbox above to be set
}
]
}