Skip to content
This repository has been archived by the owner on Jan 25, 2023. It is now read-only.

dy/templize

Repository files navigation

templize

HTML reactive template parts for any DOM elements with expressions and directives.

Based on Template Instantiation and DOM-parts specs.

Features

Extends template parts with the following:

  • Works with any elements, not just <template>;
  • Supports reactive fields;
  • Provides expression processor;
  • Enables loops, conditions;
  • Exposes directives API;
  • Plugs as vanilla ESM, doesn't require tooling.

Usage

It can be used as module via npm i templize or in HTML directly:

<script type="importmap">{ "imports": { "templize": "path/to/templize.js" }}</script>

<div id="foo" class="foo {{y}}">{{x}} world</div>

<script type="module">
  import templize from 'templize'

  templize(document.getElementById('foo'), { x: 'Hello', y: 'bar'})
  // <div id="foo" class="foo bar">Hello world</div>
</script>

API

const [params, update] = templize(element, init?);

params is proxy reflecting template fields values. Changing any of its props updates / rerenders fields.
update can be used for bulk-updating multiple props.
init is the initial state to render the template. It can include reactive values, see reactivity.

Reactivity

Template fields support the following async/reactive values:

  • Promise/Thenable
  • AsyncIterable
  • Observable/Subject

Update happens when any param changes:

<div id="done">{{ loading ? '...' : result }}</div>

<script type="module">
  import templize from 'templize'
  import { signal } from '@preact/signals'

  const loading = signal(false), result = signal(false)
  templize(document.querySelector('#done'), { loading, result })
  
  setTimeout(() => (loading.value = true, result.value = 'done'), 1000)

  // <div id="done">...</div>
  // ... 1s after
  // <div id="done">done</div>
</script>

This way, for example, @preact/signals or rxjs can be streamed directly to element attribute or content.

Note: observers don't require disposal, since they're connected in weak fashion. Once element is disposed, observables are disconnected.

Expressions

Templize enables expressions via default expression processor:

<header id="title">
  <h1>{{ user.name }}</h1>
  Email: <a href="mailto:{{ user.email }}" onclick="{{ e => { event.preventDefault(); await sendEmail(user.email); } }}">{{ user.email }}</a>
</header>

<script>
  import templize from 'templize';

  templize(
    document.querySelector('#title'),
    { user: { name: 'Hare Krishna', email: 'krishn@hari.om' }}
  )
</script>

It supports the following field expressions with common syntax:

Part Expression
Value {{ foo }}
Property {{ foo.bar?.baz }}, {{ foo[bar] }}
Call {{ foo.bar(baz, qux) }}
Boolean {{ !foo && bar || baz }}
Ternary {{ foo ? bar : baz }}
Primitives {{ "foo" }}, {{ true }}, {{ 0.1 }}
Comparison {{ foo == 1 }}, {{ bar >= 2 }}
Math {{ a * 2 + b / 3 }}
Pipe {{ bar | foo }}{{ foo(bar) }}

Attributes

Processor makes assumptions regarding how attribute parts set values.

  • hidden="{{ boolean }}" boolean values set or remove attribute.
  • onClick="{{ function }}" assigns onclick handler function (no need to call it, unlike in html).
  • class="{{ classes }}" can take either an array or a string.
  • style="{{ styles }}" can take either an object or a string.

Other attribute values cast to strings.

Directives

Templize recognizes shortcut directives via :<attr> (similar to vue).

Loops

Iterating over set of items can be done with each directive:

<ul>
  <li :each="{{ item, index in items }}" id="item-{{item.id}}" data-value="{{item.value}}">{{item.label}}</li>
</ul>

Conditions

To optionally display an element, there are if, else-if, else directives.

<span :if="{{ status == 0 }}">Inactive</span>
<span :else-if="{{ status == 1 }}">Active</span>
<span :else>Finished</span>

Note: text conditions can be organized via ternary operator:

<span>Status: {{ status === 0 ? 'Active' : 'Inactive' }}</span>

Adding directives

To register a directive, directive(name, onCreate) function can be used:

import templize, { directive } from 'templize'

directive('inline', (instance, innerTplPart, state) =>
  innerTplPart.replaceWith(innerTplPart.template.createInstance(state))
)

Interop

Templize supports any standard template parts processor:

const params = templize(element, initState, {
  createCallback(element, parts, state) {
    // ... init parts / parse expressions
  },
  processCallback(element, parts, state) {
    // ... update parts / evaluate expressions
  }
})

Any external processor can be used with templize, eg. @github/template-parts:

import templize from 'templize'
import { propertyIdentityOrBooleanAttribute } from '@github/template-parts'

const params = templize(
  document.getElementById('foo'),
  { x: 'Hello', hidden: false },
  propertyIdentityOrBooleanAttribute
)
params.hidden = true

Templize expression processor can also be used with other template instancing libraries as:

import { TemplateInstance } from '@github/template-parts'
import { processor } from 'templize'

const instance = new TemplateInstance(document.querySelector('my-template'), {}, processor)

Or it can be used with proposal polyfill:

import 'templize-instantiation-polyfill'
import { processor } from 'templize'

document.defineTemplateType('my-template-type', processor)

Dependencies

  • template-parts − compact template parts ponyfill.
  • subscript − fast and tiny expressions parser.
  • sube − subscribe to any reactive source.
  • element-props − normalized element properties setter.

Buddies

  • spect − selector observer, perfect match for organizing flexible native DOM templates.
  • value-ref − reactive value container with reactivity, useful for state management.
  • subscribable-things − reactive wrappers for various APIs.

Neighbors

  • stampino − small HTML template system based on lit-html.

🕉