Social Media Photo by Kristine Weilert on Unsplash
Please ask questions in the dedicated discussions repository, to help the community around this project grow ♥
The hyperHTML strength & experience without its complexity 🎉
If you want 90% of functionalities offered by lightetrhtml for 2/3rd of its size, check µhtml, which is also used in "micro custom elements" µce library, hence µce-template too, plus "micro land" µland 🦄
?boolean=${value}
syntax from µhtml has landed in lighterhtml too. Feel free to read this long discussion to better understand why this syntax is better, or necessary.Even if sharing 90% of hyperHTML's code, this utility removed all the not strictly necessary parts from it, including:
Component
, no define
and/or intents, no connect
or disconnect
, and no promises (possibly in later on), everything these days can be easily handled by hooks, as example using the dom-augmentor utilityhtml
before any template when you need it. However, the {html: string}
is still accepted for extreme cases.Removing these parts made the output smaller in size (less than 6K) but it also simplified some underlying logic.
Accordingly, lighterhtml delivers raw domdiff and domtagger performance in an optimized way.
If you don't believe it, check the DBMonster benchmark 😉
In lit-html, the html
function tag is worthless, if used without its render
.
In lighterhtml though, the html.node
or svg.node
tag, can be used in the wild to create any, one-off, real DOM, as shown in this pen.
// lighterhtml: import the `html` tag and use it right away
import {html} from '//unpkg.com/lighterhtml?module';
// a one off, safe, runtime list 👍
const list = ['some', '<b>nasty</b>', 'list'];
document.body.appendChild(html.node`
<ul>${list.map(text => html.node`
<li>${text}</li>
`)}
</ul>
`);
Strawberry on top, when the html
or svg
tag is used through lighterhtml render
, it automatically creates all the keyed performance you'd expect from hyperHTML wires, without needing to manually address any reference: pain point? defeated! 🍾
If you are looking for Custom Elements like events out of the box, lighterhtml-plus is your next stop 👍
Following, the usual multi import pattern behind every project of mine:
lighterhtml
CDN utility: <script src="https://unpkg.com/lighterhtml"></script>
, and const {render, html, svg} = lighterhtml
import {render, html, svg} from 'https://unpkg.com/lighterhtml?module'
import {render, html, svg} from 'lighterhtml'
const {render, html, svg} = require('lighterhtml')
The module exports the following:
html
tag function to create any sort of HTML content when used within a render
call. It carries two extra utilities, html.for(ref[, id])
, to hard-reference a specific node, and html.node
to create one-off dom nodes in the wild without using the render
.svg
tag function to create any sort of SVG content when used within a render
call. It carries two extra utilities, svg.for(ref[, id])
, to hard-reference a specific node, and svg.node
to create one-off dom nodes in the wild without using the render
.render(node, fn|Hole)
to pollute a node
with whatever is returned from the fn
parameters, including html
or svg
tagged layoutHole
class for 3rd parts (internal use)You can test live a hook
example in this Code Pen.
hyperHTML.wire(ref[, type:id])
unless you explicitly ask for it via html.for(ref[, id])
or svg.for(ref[, id])
, where in both cases, the id
doesn't need any colon to be unique. This creates content hard wired whenever it's needed.ref=${object}
attribute works same as React, you pass an object via const obj = useRef(null)
and you'll have obj.current
on any effect. If a callback is passed instead, the callback will receive the node right away, same way React ref does.aria
, as in aria=${object}
, aria attributes are applied to the node, including the role
one.data
, as in data=${object}
, the node.dataset
gets populated with all values..dataset
, as in .dataset=${object}
, the node.dataset
gets populated with all values.define
, are not implemented. Most tasks can be achieved via hooks.onconnected
and ondisconnected
special events are available only in lighterhtml-plus. These might come back in the future but right now dom-augmentor replaces these via useEffect(callback, [])
. Please note the empty array as second argument.Component
can be easily replaced with hooks or automatic keyed rendersArray
such as [listener, {once: true}]
, the second entry of the array will be used as option.const {render, html} = lighterhtml;
// all it takes to have components with lighterhtml
const Comp = name => html`<p>Hello ${name}!</p>`;
// for demo purpose, check in console keyed updates
// meaning you won't see a single change per second
setInterval(
greetings,
1000,
[
'Arianna',
'Luca',
'Isa'
]
);
function greetings(users) {
render(document.body, html`${users.map(Comp)}`);
}
Excluding the already mentioned removed parts, everything else within the template literal works as described in hyperHTML documentation.
Live on Code Pen.
import {render, html} from '//unpkg.com/lighterhtml?module';
document.body.appendChild(
// as unkeyed one-off content, right away 🎉
html.node`<strong>any</strong> one-off content!<div/>`
);
// as automatically rendered wired content 🤯
todo(document.body.lastChild);
function todo(node, items = []) {
render(node, html`
<ul>${
items.map((what, i) => html`
<li data-i=${i} onclick=${remove}> ${what} </li>`)
}</ul>
<button onclick=${add}> add </button>`);
function add() {
items.push(prompt('do'));
todo(node, items);
}
function remove(e) {
items.splice(e.currentTarget.dataset.i, 1);
todo(node, items);
}
}
You got 'em, just bind render
arguments once and update the element content whenever you feel like.
Compatible with the node itself, or its shadow root, either opened or closed.
const {render, html} = lighterhtml;
customElements.define('my-ce', class extends HTMLElement {
constructor() {
super();
this.state = {yup: 0, nope: 0};
this.render = render.bind(
null,
// used as target node
// it could either be the node itself
// or its shadow root, even a closed one
this.attachShadow({mode: 'closed'}),
// the update callback
this.render.bind(this)
);
// first render
this.render();
}
render() {
const {yup, nope} = this.state;
return html`
Isn't this <strong>awesome</strong>?
<hr>
<button data-key=yup onclick=${this}>yup ${yup}</button>
<button data-key=nope onclick=${this}>nope ${nope}</button>`;
}
handleEvent(event) {
this[`on${event.type}`](event);
}
onclick(event) {
event.preventDefault();
const {key} = event.currentTarget.dataset;
this.state[key]++;
this.render();
}
});
Born at the beginning of 2017, hyperHTML matured so much that no crucial bugs have appeared for a very long time.
It has also been used in production to deliver HyperHTMLElement components to ~100M users, or to show W3C specifications, so that in case of bugs, hyperHTML will most likely be on the fast lane for bug fixes, and lighterhtml will eventually follow, whenever it's needed.
On top of this, most modules used in lighterhtml are also part of hyperHTML core, and the ./tagger.js file is mostly a copy and paste of the hyperHTML ./objects/Update.js one.
However, as tech and software evolve, I wanted to see if squashing together everything I know about template literals, thanks to hyperHTML development, and everything I've recently learned about hooks, could've been merged together to deliver the easiest way ever to declare any non-virtual DOM view on the Web.
And this is what lighterhtml is about, an attempt to simplify to the extreme the .bind(...)
and .wire(...)
concept of hyperHTML, through a package that requires pretty much zero knowledge about those internals.
lighterhtml is also relatively new, so that some disabled functionality might come back, or slightly change, but if you like the idea, and you have tested it works for your project, feel free to ditch hyperHTML in favor of lighterhtml, so that you can help maturing this project too.
µhtml is a great way to start playing around with most lighterhtml features. As it's simply a subset, you can eventually switch to lighterhtml later on, whenever you miss, or need, some extra feature.
For a complete comparison of features and libraries around my repositories, please have a look at this gist.
This session covers all major breaking changes and added features.
I am afraid this major was necessary due recent bugs/discoveries that made me rethink some practice and patch.
data
helper could conflict with some node such as <object>
, hence it has been replaced by the .dataset
utility. Since element.dataset = object
is an invalid operation, the sugar to simplify data-
attributes is now never ambiguous and future-proof: <element .dataset=${...} />
it is.Because of these breaking changes, all libraries around lighterhtml will gradually bump major version too, pointing at this paragraph of the README.
data
and aria
attributes.Since the introduction of .setter=${value}
made special cases such as data=${...}
and props=${...}
redundant, as it's always possible to simply attach any kind of data via .data=${...}
or .props=${...}
, version 3 enhances the declarative power of the template to HTML translation.
// the aria special case
html`<div aria=${{labelledBy: 'id', role: 'button'}} />`;
//=> <div aria-labelledby="id" role="button"></div>
// the *deprecated* dataset special case
html`<div data=${{key: 'value', otherKey: 'otherValue'}} />`;
//=> <div data-key="value" data-other-key="otherValue"></div>
// the *new* dataset special case
html`<div .dataset=${{key: 'value', otherKey: 'otherValue'}} />`;
//=> <div data-key="value" data-other-key="otherValue"></div>
This means the previous data=${...}
behavior should be substituted with .dataset=${...}
and it's now possible to better reflect declarative intents in nodes, simplifying both data-*
attributes and aria-*
ones.
Please note using data-name=${value}
, as well as aria-name=${value}
is still handled like any other regular attribute, hence it will work as expected, actually faster when the values don't change frequently, as both aria
and data
special cases simply loop through the object keys and assign their values to node's attributes.
Until version 2.1, there was no way to define different options for any listener. The el.addEventListener(type, listener, false)
was the only kind of operation possible behind the scene.
In v2.1 though, whenever a second option is needed, it is now possible to pass an Array
instead, in the form [listener, {once: true}]
, as example, or [listener, true]
to use capture instead of bubbling, and so on and so forth.
Bear in mind, specially for the once
case, if the listener is different per each update, like onclick=${[() => stuff(), {once: true}]}
, it will be set each time that update happens, so that in this case is better to use always the same listener, either via outer scope callback, or via reference, using useRef
and the handleEvent
pattern, as example.
If you never needed to add a different second option, there is nothing you should do, everything will work exactly as it did before.
render(...)
is invoked. When needed, which is the minority of the cases, you need to explicitly use html.node
or svg.node
, instead of just html
or svg
. For every other cases, use render(where, what)
.render(where, what)
does not need a callback anymore. You can now render(node, html`<p>content</p>`)
right away. If a callback is provided, that will still be invoked.useHook
as it's unnecessary since you can use useRef
through html.for(...)
or svg.for(...)
within any useRef
provided by your library of choice (i.e. dom-augmentor)inner.html/svg
has been removed, as completely unnatural and error prone (just use html
anywhere, it'll work).html
and svg
template literals tags, now offer both .for(ref[, id])
and .node
, to either retain the same content (keyed render) or create fresh new nodes out of the box as one-off operation (via .node
).Removed transform
export and made default domtagger customizable via custom
export.
import { custom } from 'lighterhtml';
const { html, render } = custom({
// the domtagger attributes handler
attribute: callback => (node, name, original) => {
// return a function that will handle the attribute value
// the function will receive just the new value
if (name === 'double')
return value => {
node[name] = value + value;
};
// the received callback is usable as return fallback
return callback(node, name, original);
},
// the domtagger any-content handler
any: callback => (node, childNodes) => {
// return a function that will handle handle all special cases
// the function will receive just the new *hole* value
if (node.nodeName === 'CUSTOM') {
return value => {
node.appendChild(value);
};
}
// the received callback is usable as return fallback
return callback(node, childNodes);
},
// the domtagger text for text only cases
text: callback => (node) => {
// return a function that will handle handle text content cases
// the function will receive just the new text value
if (node.nodeName === 'WRAP') {
return value => {
node.textContent = `(${value})`;
};
}
// the received callback is usable as return fallback
return callback(node);
},
// optionally you can use the special transform handler too
// in this case, and in V1, the callback is just the String one
transform: callback => markup => callback(markup),
// same goes for convert, with the callback being the one
// originally used to "convert" the template from Array to HTML
// see: https://github.com/WebReflection/domtagger/issues/17#issuecomment-526151473
convert: callback => template => callback(template)
});