2019-06-07
Tagged templates
Template literals have been a part of the specs since ES2015.
'We do not ' + need + ' to concatenate ' + strings + 'like this ' + anymore
`This ${feels} much ${smoother}`
Writing HTML strings in javascript was frowned upon once upon a time. It is not anymore, thanks to the popularity of react and JSX. Could we just use it to write our templates, and add it to the DOM with the good old .innerHTML
? You could, but that has not come back in fashion yet. It probably never will.
There are however ways to use template literals for the DOM and they all use so called tagged templates:
const taggedTemplateExample = (strings, ...arguments) =>
console.log({ strings, arguments })
const name = 'Bob'
const object = { foo: 1 }
taggedTemplateExample`My name is ${name}. Here is an ${object}.`
returns
{
arguments: [ "Bob", { foo: 1 } ],
strings: [ "My name is ", ". Here is an ", "." ]
}
This is a bit of a stupid example but it does show how arguments and strings are passed to tagged templates. It is just a function that takes an array of strings as the first argument. That is the string inside the template, cut up at every variable. All subsequent arguments are the variables. And they do not need to be strings.
This opens up a lot of possibilities. ObservableHQ, for example, makes good use of tagged templates for cells with markdown, svg or html. There are also a couple of interesting frontend libraries that use them.
htm
htm is JSX-like syntax in plain JavaScript - no transpiler necessary.
It is created by the guy behind preact. The following example is taken from his article WTF is JSX. This JSX:
/** @jsx h */
const foo = <div id="foo">Hello!</div>
once it has been transpiled to javascript, looks like this:
const foo = h('div', {id:"foo"}, 'Hello!')
The HTML looking part becomes arguments to an h
function.
What h
, does is not the problem of JSX whose job is just to parse the HTML-looking code into arguments for h
.
htm
works the same way. Its a tagged template that converts the content into arguments that can be passed to h
. Here is an example of an h
function that just logs the arguments it gets:
import htm from 'htm'
const h = (type, props, ...children) =>
console.log({ type, props, children })
const html = htm.bind(h)
html`<h1 id=hello>Hello world!</h1>`
Result:
{
type: 'h1',
props: { id: 'hello' },
children: ['Hello world!']
}
This is not very useful. Let's use preact's h
function:
import htm from 'htm'
import { h, render } from 'preact'
const html = htm.bind(h)
render(html`<App />`, document.body)
htm
lets you use virtual DOM libraries, such as preact, without JSX. As tagged templates are part of the javascript specs (as opposed to JSX), there is no need to use a transpiler.
hyper(HTML)
The DOM Is NOT Slow, Your Abstraction Is
hyperHTML
does not need an h
function, it uses the HTML <template>
tag.
Another template?
Yes, this starts getting confusing. So far we have talked about "tagged templates" and "template literals" and now the "template" tag. This one is part of the HTML specification. You add it to your DOM like this:
<template id="templ">
<h1>Hello <span class="templ-name"></span></h1>
</template>
Used just like this, it will be parsed by the browser but you will not see it in your page until you explicitly clone it and append it to the body.
const template = document.getElementById('templ')
const clone = document.importNode(template.content, true)
const nameSpan = clone.querySelector('.templ-name')
nameSpan.textContent = 'world'
document.body.appendChild(clone)
That is what hyperHTML uses under the hood. The following function would create a template similar to the one above.
const Welcome = props =>
hyperHTML.wire()`<h1>Hello, ${props.name}</h1>`
When the name changes, the template does not need to be rerendered. Only the relevant textContent
changes. The same template can be cloned several times. The minimal abstraction makes this approch really fast.
This is all very nice, you may think, but this only lets you create "dumb components" (in react speak). What about statefull components? Can you do that?
Yes you can. First you need to create a new element class by extending the generic HTMLElement
to add a setState
function.
class HyperElement extends HTMLElement {
constructor(...args) {
super(...args)
this.html = hyperHTML.bind(this)
}
render() {}
setState(state) {
flatstate.setState.call(this, state)
this.render()
}
}
Then extending this new element, you can create your stateful component.
class StatefulComponent extends HyperElement {
connectedCallback() {
this.state = {}
this.render()
}
onChange(e) { this.setState({ value: e.target.value }) }
render() {
return this.html`
<div>
<input oninput=${this.onChange.bind(this)} />
<p>${this.state.value}</p>
</div>`
}
}
It does not look very different from how you would do it in react. But without all the tooling and transpiling. What you are doing is essentially creating a webcomponent.
Define your custom element:
customElements.define(
'stateful-component',
StatefulComponent
)
document.body.appendChild(new StatefulComponent)
And use it directly in your page.
<stateful-component></stateful-component>
There is also a library to render on the server, viperHTML.
const viperHTML = require('viperhtml')
const component = viperHTML`
<input onchange=${ e => alert(e.target.value) } />
`
console.log(component.toString())
Will render this:
<input onchange="return (e => alert(e.target.value)).call(this, event)">
lit-html
Efficient, Expressive, Extensible HTML templates
import {html, render} from 'lit-html'
const myTemplate = name => html`<p>Hello ${name}</p>`
render(myTemplate('World'), document.body)
This one is very similar to hyperHTML. The main difference is that it is backed by google. Thus more likely to be widely adopted. lit-html
is the templating language in the latest version of Polymer, their webcomponent framework.
There are a couple of semantic differences with hyperHTML, notably a dot in front of attributes and a @
to add event listeners:
html`<img .src=${src} />`
html`<button @click=${clickHandler}>Click Me!</button>`
For looping over elements, you can use .map
, as in hyperHTML:
html`<ul><${items.map(item => html`<li>${item}</li>`}</ul>`
lit-html also has a repeat
directive to do the same. It takes an argument defining the unique id of each element of the list.
html`<ul><${repeat(
items,
item => item.id,
item => html`<li>${item}</li>`
)}</ul>`
The purpose is the same as the key
attribute in react: to avoid rerendering if the order changes, sortable lists for example.
For stateful components, there is a sister library, lit-element that includes lit-html and is used to create webcomponents.
class StatefulComponent extends LitElement {
static get properties() {
return { state: { type: String } }
}
constructor() {
super()
this.state = ''
}
onChange(e) { this.state = e.target.value }
render() {
return html`<p>${this.state}</p>
<input @input=${this.onChange} />`
}
}
Integration with state management librairies such as redux is also pretty straight forward.
import { html, render } from 'lit-html'
import store from './store'
import action from './actions'
const App = state => html`
<p>${state}</p>
<input @input=${action.setState} />
`
store.subscribe(() =>
render(App(store.getState(), document.body)))
If you, like me, feel the amount of tooling for frontend development has gotten out of hand, these simpler solutions should be a welcome evolution. I know what I will use for my next project...