Templates with automatic registration of Custom Elements.
const template = html`<${CustomElement}></${CustomElement}>`;
Inspired by JSX in general and htm in particular.
There are 2 main reasons Web Components need something like this:
- Lack of scoping when registering Custom Elements which creates issues in tests and makes it impossible to have 2 different components with the same name.
- Inability to have 2 different versions of the same Custom Element when refactoring from an old to a new version, especially when having nested node modules.
You need to wrap the lit-html:
import { LitElement, html as litHtml } from '@polymer/lit-element';
import takeCareOf from 'carehtml';
const html = takeCareOf(litHtml);
class MySearchBar extends LitElement {
render() {
return html`
<${MyInput} name="query"></${MyInput}>
<${MyButton}>
<${MyIcon} icon="search"></${MyIcon}>
Search
</${MyButton}>
`;
}
}
Wrapping the lit-html function is extra work which might seem unnecessary in the user code, but that allows to decouple
carehtml
fromlit-html
, primarily in terms of npm dependencies. This allows to usecarehtml
with any version oflit-html
and developcarehtml
with its independent release cycle.
In fact it can work with any other templating library which relies on tagged templates.
For example with htm
allowing to mix Custom Element classes with Preact components (and any other components supported by htm
including React ones).
This is an example taken from the htm
docs with one change: instead of simple <button>
there is a Custom Element based Button
from new Material Web Components.
import htm from 'htm';
import { h, Component, render } from 'preact';
import { Button } from '@material/mwc-button';
import takeCareOf from 'carehtml';
const html = takeCareOf(htm.bind(h));
class App extends Component {
addTodo() {
const { todos = [] } = this.state;
this.setState({ todos: todos.concat(`Item ${todos.length}`) });
}
render({ page }, { todos = [] }) {
return html`
<div class="app">
<${Header} name="ToDo's (${page})" />
<ul>
${todos.map((todo) => html`<li>${todo}</li>`)}
</ul>
<${Button} onClick=${this.addTodo.bind(this)}>Add Todo</${Button}>
<${Footer}>footer content here<//>
</div>
`;
}
}
render(html`<${App} page="All" />`, document.body);
import { html as litHtml, render } from 'lit-html';
import takeCareOf from 'carehtml';
const html = takeCareOf(litHtml);
describe('MyMixin', () => {
it('does something', () => {
class MyElement extends MyMixin(HTMLElement) {
// define extra behavior
}
// create fixture
// (html`` in this context returns a lit-html TemplateResult as if it was lit-html itself)
const element = fixture(html`<${MyElement}></${MyElement}>`);
// test mixin/element behavior
});
});
function fixture(litTemplate) {
// please use smth like this in real life
// https://open-wc.org/recommendations/testing-helpers.html#test-a-custom-element-with-properties
const wrapper = document.createElement('div');
render(litTemplate, wrapper);
document.body.appendChild(wrapper);
return wrapper.children[0];
}
Runtime performance is not the key requirement for carehtml
since the end goal is to compile the code and have static and still unique tag names in the production code.
But some numbers might be interesting to show the impact of such solution on local development and the potential runtime usage in production for projects that want to stay compilation-free.
There are 2 things which carehtml
can slow down and which can be measured: creating a template and rendering a template.
Both can be measured together as well.
The original idea was that the benchmarks need to compare the most minimalistic template possible, e.g. <my-element></my-element>
where MyElement
does not render any internal template, otherwise the benchmarks will measure the DOM update caused by the internal template instead of the carehtml
overhead.
It turned out to be quite difficult to see the carehtml
impact in such benchmarks, because it's insignificant as compared to even rendering such a minimalistic <my-element></my-element>
template.
You can play around with this by using yarn bench:create-and-render:chrome
script and alike and modifying the benchmarks/index.html
to your needs, e.g. by removing the constructors of the measured elements.
The only thing that makes sense to measure in this situation is the rerendering.
The idea is to check if it does not rerender unnecessarily second time when the classes stay the same meaning that the actual template is also the same.
That's what makes lit-html
so fast after all and carehml
should not break this essential optimisation.
In such benchmarks the <my-element></my-element>
should have an internal template which will take most of the time of each render, so that the rerendering (if it happens) is close to being 2 times slower due to that internal template being rendered again.
The end setup has MyElement
with a shadow root with 100000 divs containing some text.
The script yarn bench:create-and-render-twice
can be used to measure that.
The goal is to have the same numbers when using lit-html
directly or wrapped with carehtml
.
And it's important to measure this with lit-html 2.x
which does not have a special template string cache originally introduced for legacy browsers where string template literals were buggy.
These are the results for Chrome which clearly show no overhead on rerendering when wrapping with carehtml
:
Benchmark | Avg time | vs direct | vs wrapped | vs wrapped with classes |
---|---|---|---|---|
direct | 108.63ms - 112.34ms | - | unsure -1% - +3% -1.57ms - +2.84ms |
unsure -2% - +2% -2.42ms - +1.80ms |
wrapped | 108.66ms - 111.05ms | unsure -3% - +1% -2.84ms - +1.57ms |
- | unsure -2% - +1% -2.51ms - +0.61ms |
wrapped with classes | 109.80ms - 111.80ms | unsure -2% - +2% -1.80ms - +2.42ms |
unsure -1% - +2% -0.61ms - +2.51ms |
- |
Measurements in other browsers are similar.
For their awesome cross-browser testing automation solution!