DEV Community

Cover image for lit-html rendering implementation
Georgi Serev for This Dot

Posted on

lit-html rendering implementation

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, ', '!' ]
// }
Enter fullscreen mode Exit fullscreen mode

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>
`;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

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:

  1. Preparation - html template tag and render function
  2. Template processing - creation of <template> & lit-html Template
  3. 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);
}
Enter fullscreen mode Exit fullscreen mode
  1. Source of TemplateResult
  2. Source of html function

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();
}
Enter fullscreen mode Exit fullscreen mode

Source of render function

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
  1. Source of Template
  2. Source of templateFactory

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 */
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Source of NodePart

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Source of TemplateInstance

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!

lit-html GitHub repository

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.

Top comments (0)