DOM templating, done right


Keywords
dom, html, data, template
License
ISC
Install
npm install tagalong@2.1.1

Documentation

tagalong

Tagalong is a tool for creating progressively enhanced HTML templates. Write your content in HTML, add attributes that describe how data maps to each element, and update the DOM with a single JavaScript call. Tagalong uses morphdom under the hood, minimizing the number of changes to the DOM and limiting layout thrashing.

Given this HTML:

<div id="people">
  <h1>
    <span t-text="items.length">1</span>
    <span t-text="items.length === 1 ? 'person' : 'people'">person</span>
  </h1>
  <ul>
    <li t-each="people">
      <span t-text="name">Rosie</span> the
      <span t-text="occupation">Riveter</span>
    </li>
  </ul>
</div>

You can render new data like so:

tagalong.render('#people', {
  people: [
    {name: 'Rosie', occupation: 'Riveter'},
    {name: 'Joe', occupation: 'Plumber'},
    {name: 'Jill', occupation: 'Astrophysicist'},
  ]
});

And the result should look something like this:

<div id="people">
  <h1>
    <span>3</span>
    <span>people</span>
  </h1>
  <ul>
    <li><span>Rosie</span> the <span>Riveter</span></li>
    <li><span>Joe</span> the <span>Plumber</span></li>
    <li><span>Jill</span> the <span>Astrophysicist</span></li>
  </ul>
</div>

API

tagalong.render(root [, data [, context]])

The render function accepts either an element reference or CSS selector as its root, an optional data value, and an additional, optional context object. This function renders the data into the root element and returns an update function that can be used to render new data:

var render = tagalong.render('#template', data);
// later
render(otherData);

Because tagalong modifies the DOM and removes all t- attributes, you must use the returned render function if you wish to update the element with new data.

Attributes

The following attributes are the "control structures" of tagalong template elements. You can use them on any child element of the root (see above).

t-text="expression"

Sets the text of the element to the value of the JavaScript expression:

<div id="template">
  <span t-text="name">foo</span>
</div>
<script>
tagalong.render('#template', {name: 'bar'});
</script>

Alternately, you can embed expressions in the text of any node with JavaScript wrapped in {{ and }}:

<div id="template">
  Hello, {{ world.toUpperCase() }}!
</div>
<script>
tagalong.render('#template', {world: 'Earth'});
</script>

t-if="expression" and t-else

Conditionally includes an element in the output if the JavaScript expression is truthy. An adjacent element with a t-else attribute will be shown only if the t-if expression is falsy.

<div id="template">
  <h1 t-if="error" class="error">Error: <span t-text="error"></span></h1>
  <h1 t-else>All good!</h1>
</div>
<script>
// this will show the first <h1> and hide the second
var render = tagalong.render('#template', {error: 'something bad happened'});
// and vice-versa
render({error: null});
</script>

t-each="expression"

Repeats the element for each value in the evaluated JavaScript expression:

<ul id="people">
  <li t-each="people">
    <span t-value="name">Name</span>
  </li>
</ul>
<script>
tagalong.render('#people', {
  people: ['Jill', 'Jane', 'Jack']
});
</script>

t-foreach="expression"

Repeats the element's children for each value in the evaluated JavaScript expression:

<div id="template">
  <dl t-foreach="terms">
    <dt t-text="name">Term</dt>
    <dd t-text="desc">Description</dd>
  </dl>
</div>
<script>
tagalong.render('#template', {
  terms: [
    {name: 'fubar', desc: 'F*cked Up Beyond All Recognition'},
    {name: 'foo bar', desc: 'Metasyntactic variables'}
  ]
});

t-with="expression"

Changes the context of nested expressions in the element's children.

<div id="template">
  <h1 t-with="items">
    <span t-value="length">0</span>
    item<span t-if="length !== 1">s</span>
  </h1>
  ...
</div>
<script>
tagalong.render('#template', {
  items: ['a', 'b', 'c']
});
</script>

t-as="symbol"

Aliases whatever data it gets (or "creates", as in t-each, t-foreach, or t-with elements) to the named symbol, which can be referred to in child expressions by name. This can be used to make nested expressions easier to read, or simplify accessing numeric indices or properties that contain odd characters:

<div id="template">
  <ul t-foreach="items" t-as="item">
    <li t-value="item[0]"></li>
  </ul>
</div>
<script>
tagalong.render('#template', {
  items: [
    ['foo', 1],
    ['bar', 2],
    ['baz', 0]
  ]
});
</script>

Behind the scenes, this just sets the property named by symbol on the context object passed to tagalong.render() while that element is rendering.

t-class="expression"

Class name expressions can evaluate to either an object or an array. If the expression evaluates to an object, then the keys are used as class names if their values are truthy:

<section t-class="{regional: true, onshore: onshore}">
</section>

If the result is an array, the values are joined with spaces and used as-is.

t-style="expression"

Style expressions can evaluate to either an object or an array. If the expression evaluates to an object, then the key/value pairs are formatted as CSS properties, separated with semicolons:

<ol id="toc">
  <li t-each="sessions" t-style="{color: color}">
    <a t-href="'#' + id" t-text="text">Section</a>
  </li>
</ol>
<script>
tagalong.render('#toc', {
  sessions: [
    {id: 'intro', name: 'Welcome'},
    {id: 'breakout', name: 'Breakout Session', color: 'pink'},
    {id: 'speaker1',  name: 'Speaker: Jane Goodall'}
  ]
});
</script>

If the result is an array, the values are assumed to be property: value pairs and are joined with semicolons.

Other Attributes

Any other attribute with a t- prefix is evaluated as a JavaScript expression and has its t- prefix stripped in the output, e.g.

<input name="first_name" t-value="first_name">
<input name="attending" t-checked="attending ? 'checked' : null">

Events

Event handling can happen in three different ways:

  1. Use event delegation to capture bubbling events at the top level of your template, which obviates the need for listeners on individual elements, e.g.

    var delegate = function(selector, callback) {
      return function(e) {
        if (e.target.matches(selector)) {
          callback.apply(this, arguments);
        }
      };
    };
    root.addEventListener('click', delegate('a', function(e) {
      alert('clicked:', e.target.href);
    }));

    ⚠️ This won't work with events that don't bubble, such as focus and blur.

  2. Use t-on* attributes to bind your listeners to template data:

    <ul id="root">
      <li t-each="items">
        <a t-onclick="(item, event) => click(item)">{{ . }}</a>
      </li>
    </ul>
    <script>
    var render = tagalong.render('#root', {
      items: [
        'foo',
        'bar',
        'baz'
      ]
    }, {
      click: function(d) {
        alert('clicked', d);
      }
    });
    </script>

    Note that "unqualified" listener references (such as click(), above) must be present in the context (3rd) argument to tagalong.render(). If you want to call a function of the data, you can do it like this:

    <a id="link" t-onclick="(d, e) => d.click(e)">hi</a>
    <script>
    tagalong.render('#link', {
      click: function(e) {
      }
    });
    </script>
  3. Use on* attributes. This is not suggested.

<t-template> Custom Element

Tagalong ships with a <t-template> custom element:

<t-template id="people">
  <h1>
    <span t-text="items.length">0</span>
    Member<span t-if="items.length !== 1">s</span>
  </h1>
  <ul>
    <li t-each="items">
      <span t-text="name">Rosie</span> the
      <span t-text="occupation">Riveter</span>
    </li>
  </ul>
</t-template>

Then, set the data either by setting the data attribute (any JavaScript or JSON expression will do), or set the data property directly:

document.getElementById('people').data = {
  people: [
    {name: 'Rosie', occupation: 'Riveter'},
    {name: 'Joe', occupation: 'Waiter'},
    {name: 'Jill', occupation: 'Welder'},
  ]
};