lit-html rendering implementation
Georgi Serev
Posted on October 12, 2019
In a world of dominant large UI frameworks and libraries, a simple solution is attempting to combine the ergonomics of the existing technologies and the power of the modern web standards.
The aim of this article is to give you an insight of the core concepts of lit-html's rendering process. But, before that:
What is lit-html?
Note: In case you are familiar with lit-html, you can skip this section.
lit-html is a templating library which makes use of the built-in browser HTML parser rather than incorporating standalone one. Internally it creates <template>
elements from user-defined string literals, and inserts and/or updates the data provided on render
where needed. This makes the library not only a good performer but also extremely small!
Template literals and tagged templates
Before moving to the core part of this article, it is important to cover the not-so-popular tagged template literals which are a more advanced form of template literals. The functionality allows the user to inspect the different parts of a template literal -- the static string parts and the interpolated data. The tag itself is represented as a function:
function hello(strings, name, surname) {
return {
strings,
name,
surname
};
}
const name = 'John';
const surname = 'Doe'
const obj = hello`Hello, ${name} ${surname}!`;
console.log(obj);
// Output:
// {
// name: 'John',
// surname: 'Doe',
// strings: [ 'Hello, ', '!' ]
// }
Note that the tag function (template tag) doesn't neccesarily return a string. In our case, we return an object with the tag function input data.
A simple template
Now, that we have basic understanding of what lit-html is and how tagged templates work, let's create a small test template for the sake of consistency. First, we should import the html
template tag from lit-html
. Then, we can write a function that returns a template literal which will represent the lit-html template we want.
import { html } from 'lit-html';
const badge = (title) => html`
<div class="badge">
<p>${title}</p>
</div>
`;
Note: lit-html supports SVG templates as well via the svg
tag
Last, we would like to render the template somewhere. For this purpose, we will have to import one more function called render
, again from lit-html
. As its name indicates, it should help us display our template on the screen:
import { html, render } from 'lit-html';
//...
render(badge('Admin'), document.body);
The function itself accepts a template and a container as its first two arguments. After execution we should have our admin badge added in the body of the page. Simple, isn't it? Okay, let's take a look how this works behind the scenes.
Further information
If you are interested in expanding your lit-html knowledge before learning about rendering, you can take a look at these:
- GitHub repos list app online demo by Martin Hochel
- Article example online demo
- Official lit-html docs
Rendering implementation
Disclaimer: The article is written based on lit-html v1.1
We already learnt how we can write a simple lit-html templates via the html
template tag and the render
function. Now we will explore the internals of the library. Note that we won't cover all of the details but the core concepts. The idea is to get an insight how this thing runs. In the process we will include code snippets of the different phases which are taken from lit-html source code. However, they are greatly simplified, so be advised.
We can informally separate the process in three parts:
- Preparation -
html
template tag andrender
function - Template processing - creation of
<template>
& lit-htmlTemplate
- Creating a template instance -
TemplateInstance
& cloning
Let's get started!
1. Preparation
In the very beginning, let's explore what the html
template tag does. We will introduce the TemplateResult
which is nothing more but a wrapper of the static string parts and the values from the tag function. Additionally, it keeps a reference to a TemplateProcessor
and it has a method which generates a <template>
called getTemplateElement
. We will cover these two later. So, what lit-html does with html
template tag is to simply create a new instance of the TemplateResult
. All of this can be summarized in this code snippet:
class TemplateResult {
strings: ReadonlyArray<string>;
values: ReadonlyArray<unknown>;
processor: TemplateProcessor;
constructor(strings, values, processor) { /* ... */ }
getTemplateElement(): HTMLTemplate { /* ... */ }
}
const defaultTemplateProcessor = /* ... */
function html(strings, value): TemplateResult {
return new TemplateResult(strings, values, defaultTemplateProcessor);
}
Following the steps that we used in the demo, a generated TemplateResult
should be then passed to the render
function. It looks like we are almost done, but actually most of the work starts from here.
Having a dive into render
implementation, we will notice that it has access to a weak map which associates a render container with an object of type NodePart
. It acts as a cache:
const parts = new WeakMap();
function render(result: TemplateResult, container: Element | DocumentFragment) {
let part = parts.get(container);
if (part === undefined) {
// *Clear container, if full*
part = new NodePart(templateFactory);
parts.set(container, part);
part.appendInto(container);
}
part.setValue(result);
part.commit();
}
Presumably, there should be a lot of questions. Let's start with what NodePart
is. Well, NodePart
or Part
(the interface) represents a dynamic part of a template instance rendered by lit-html. Or in other words -- where our data is plugged.
As you can see in the code above, we append a Part into our container (e.g. in our demo case - the body). This happens only, if the container hasn't been used for rendering yet. If it was, the cache will already have a Part associated with it. It's interesting that if you take a look at the DOM tree at this step of the process, you will notice some empty HTML comments added to it. These are used as markers for the beginning and the end of the respective Part.
After we have our container prepared (i.e. with inserted Part), we set the TemplateResult
as pending value to the respective Part. By commit
-ing after that, the template processing triggers.
Moving forward, we will elaborate on commit
and templateFactory
in the next section.
Note: The WeakMap
will allow to have its values garbage-collected, if they aren't referenced anywhere in the code.
2. Template processing
In the first section we just mentioned about the getTemplateElement
method of the TemplateResult
. Here we will actually make use of it. What it does is simple -- join all static string parts of the template and add markers where we plan to plug data. In the end, return a <template>
. lit-html uses different types of markers depending on the place of interpolation. For instance, the content of an element will be marked with a comment of <!--{{lit-guid}}-->
type whereas an attribute -- with ATTR_NAME$lit$="{{lit-guid}}"
. If we take the template we wrote in our demo above as an example, we will end up with something like this:
<template>
#document-fragment
<div class="badge">
<p><!--{{lit-9858251939913858}}--></p>
</div>
</template>
Cool, isn't it?
Okay, nice. The next part of the chain is the templateFactory
which we passed previously on our NodePart
. It incorporates the Factory pattern with some caching as you might've already guessed from the name. The output -- a lit-html template or Template
:
class Template {
parts: TemplatePart[] = [];
element: HTMLTemplateElement;
constructor(result: TemplateResult, template: HTMLTemplateElement) {
this.element = template;
while (partIndex < result.values.length) {
// ...
this.parts.push({ type: 'node', index });
// ...
}
}
}
function templateFactory(result: TemplateResult) {
// *Check if template is in cache. If not, create a new one*
const t = new Template(result, result.getTemplateElement());
// *Add to cache*
return t;
}
What the Template
does is to process the generated <template>
from the TemplateResult
by recording/tracking the positions of the markers that we talked about earlier. That way, our lit-html template is ready to be used.
Okay, let's go back to the NodePart
and the commiting process we've been talking about.
It is important to mention that for the purposes of this article, we will cover only the process of commiting of a TemplateResult
. You might already know that a Part can accept a node, an iterable or text as well.
class NodePart implements Part {
commit(value) {
// ...
this._commitTemplateResult(value);
// ...
}
_commitTemplateResult(value) {
// Create a Template
const template = this.templateFactory(value);
if (this.value instanceof TemplateInstance && this.value.template === template) {
// *Update the instance*
} else {
// *Create the instance*
this.value = /* new instance */
}
}
}
As you can see this is where we make use of the template factory which should return a ready-to-use lit-html Template
. After that we create or update the template instance associated with the NodePart
.
3. Creating a template instance
It's time to create our TemplateInstance
. The instance is an object
that accepts a lit-html Template
and the processor we briefly mentioned in the first code snippet. Its task is to use the processor in order to create dynamic parts in the document fragment derived from the <template>
during cloning:
class TemplateInstance {
private _parts: Array<Part|undefined> = [];
processor: TemplateProcessor;
template: Template;
constructor(template: Template, processor: TemplateProcessor) { /* ... */ }
update(values: ReadonlyArray<unknown>) {
// *for each part*
// *set a value and then commit*
}
clone(): DocumentFragment {
// ...
const fragment = this.template.element.content.cloneNode(true) as DocumentFragment;
// *Use the processor and the Template's part metadata to create Parts*
return fragment;
}
}
The processor itself contains the lit-html template specific stuff like the attribute prefixes .
@
or ?
. Based on this syntax, it creates a Part -- NodePart
, AttributePart
, etc.
Finally, after we have our instance created and cloned, we can commit it which means that it is inserted to the DOM. At this point of the process you should be able to see the template rendered on the screen!
Now, on every new update, lit-html will use the exactly the instance and will modify only the values of the dynamic parts we created. Neat!
In short
- lit-html builds a HTML template by concatenating the static string parts and inserting markers where an interpolation is going to happen.
- Later, the locations of these markers are recorded in an lit-html template object.
- An instance is created. The metadata from the lit-html template is used in order to create dynamic parts within the template instance.
- Finally, the ready product is added to the DOM and the dynamic parts are being updated when new values are provided.
Conclusion
While the templating with lit-html looks very similar to what we have with the popular web UI technologies, it is vastly different behind the scenes. The lack of an additional compilation step or the need of a virtual DOM, contribute to the simplicity of this templating library which has its own place in the modern and diverse front-end world.
Enjoy this article? Head on over to This Dot Labs and check us out! We are a tech consultancy that does all things javascript and front end. We specialize in open source software like, Angular, React and Vue.
Posted on October 12, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024