Template Basics

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.

#Declaring a Component Template

Components declare their template as a static field named template:

class MyComponent extends Component
{
    static template = {};  The template must be declared static 
}

#Node Kinds

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 
    }
    

#Child Nodes

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", },

#Dynamic Content

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 
    }
}

#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",

#Binding Elements

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 
        }
    ]
}