DEV Community

Nozomu Ikuta
Nozomu Ikuta

Posted on

Exploring How lit-html Works: TemplateResult and SVGTemplateResult constructors

In this series, How lit-html works, I will explore (not explain) internal implementation of lit-html.

In the previous post, we saw that what html and svg do. They receive template literals and pass the strings and values to the corresponding constructors, TemplateResult and SVGTemplateResult.

In this post, I will explore what instances that the constructors create looks like.

TemplateResult class

Here is definitions of properties and constructor method of the TemplateResult class.

export class TemplateResult {
  readonly strings: TemplateStringsArray;
  readonly values: readonly unknown[];
  readonly type: string;
  readonly processor: TemplateProcessor;


  constructor(
      strings: TemplateStringsArray, values: readonly unknown[], type: string,
      processor: TemplateProcessor) {
    this.strings = strings;
    this.values = values;
    this.type = type;
    this.processor = processor;
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

All the arguments are simply assigned to readonly properties whose name is the same.

TemplateResult class has two prototype methods as well: getHTML() and getTemplateElement().

getHTML() method

getHTML(): string {
  const l = this.strings.length - 1;
  let html = '';
  let isCommentBinding = false;

  for (let i = 0; i < l; i++) {
    const s = this.strings[i];

    const commentOpen = s.lastIndexOf('<!--');
    isCommentBinding = (commentOpen > -1 || isCommentBinding) &&
        s.indexOf('-->', commentOpen + 1) === -1;

    const attributeMatch = lastAttributeNameRegex.exec(s);
    if (attributeMatch === null) {
      html += s + (isCommentBinding ? commentMarker : nodeMarker);
    } else {
      html += s.substr(0, attributeMatch.index) + attributeMatch[1] +
          attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] +
          marker;
    }
  }
  html += this.strings[l];
  return html;
}
Enter fullscreen mode Exit fullscreen mode

getHTML() method returns full HTML string, where bindings are modified with suffixes based on their binding types.

The bound values are replaced by the marker {{lit-RANDOM_NUMBER}}.

Note that, although RANDOM_NUMBER in the examples below differs every time, it is actually determined only once in runtime and is shared.

export const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
Enter fullscreen mode Exit fullscreen mode

Text Binding

const name = 'Nozomu Ikuta';
const templateResult = html`<div>Hello, ${name}</div>`;

console.log(templateResult.getHTML());
// => `<div>Hello, <!--{{lit-6732669937008782}}-->!</div>`
Enter fullscreen mode Exit fullscreen mode

Attribute Binding

The names of bound attributes are prepended by the suffix $lit$.

// Text Attribute Binding
const className = 'my-class';
const templateResult = html`<div class="${className}">Hello, Nozomu Ikuta</div>`;

console.log(templateResult.getHTML());
// => `<div class$lit$="{{lit-37406895828981424}}">Hello, Nozomu Ikuta</div>`
Enter fullscreen mode Exit fullscreen mode
// Boolean Attribute Binding
const bool = true;
const templateResult = html`<button type="button" ?disabled="${bool}">Click!</button>`
console.log(templateResult.getHTML())
// => `<button type="button" ?disabled$lit$="{{lit-407422159769641}}">Click!</button>`
Enter fullscreen mode Exit fullscreen mode
// Property Binding
const value = 'My Value';
const templateResult = html`<input .value=${value}>`
console.log(templateResult.getHTML())
// => `<input .value$lit$={{lit-36790084947651724}}>`
Enter fullscreen mode Exit fullscreen mode
// Event Listener Binding
const templateResult = html`<button @click=${(e) => console.log('clicked')}>Click!</button>`
console.log(templateResult.getHTML())
// => `<button @click$lit$={{lit-14297238026644}}>Click!</button>`
Enter fullscreen mode Exit fullscreen mode

If-statements are to handle comment-like strings correctly, but I will check in more depth later.

getTemplateElement() method

This method returns HTML template element whose innerHTML is the returned string by getHTML() method.

convertConstantTemplateStringToTrustedHTML function does nothing unless Trusted Types is available so I will skip in this time.

getTemplateElement(): HTMLTemplateElement {
  const template = document.createElement('template');
  template.innerHTML =
      convertConstantTemplateStringToTrustedHTML(this.getHTML()) as string;
  return template;
}
Enter fullscreen mode Exit fullscreen mode

getTemplateElement() method is used by render function, which is used when we want to insert elements based on the TemplateResult or TemplateResult into the real DOM tree.

SVGTemplateResult class

SVGTemplateResult class extends TemplateResult class and overrides the two methods.

getHTML() method

getHTML() method of SVGTemplateResult class wraps the HTML string with svg tags, so that it can be parsed in the SVG namespace.

getHTML(): string {
  return `<svg>${super.getHTML()}</svg>`;
}
Enter fullscreen mode Exit fullscreen mode

getTemplateElement() method

getHTML() method of SVGTemplateResult class remove the extra svg element that is added by getHTML() method, and returns the HTML template element.

getTemplateElement(): HTMLTemplateElement {
  const template = super.getTemplateElement();
  const content = template.content;
  const svgElement = content.firstChild!;
  content.removeChild(svgElement);
  reparentNodes(content, svgElement.firstChild);
  return template;
}
Enter fullscreen mode Exit fullscreen mode

Summary

So far, I learned the following points:

  • An instance of TemplateResult class has all arguments passed by html function as readonly properties.
  • In addition, TemplateResult class has two prototype methods by which to get HTML string and HTML template element whose innerHTML is the HTML string.
  • TemplateResult class extends TemplateResult class and overrides the two methods.

Since I skipped some lines of code which are not important to grasp the main logic, I will look into those in the next post.

Top comments (0)