The Many Ways of Templates in HTML Custom Elements
Tags: html, typescript, javascript, webdev
Published: 2020-09-13 04:40:45

Already having a couple years each in React and Angular I decided to learn **HTML Custom Elements**, the browser's native component framework. "This should be easy," I thought, "Just tell me where to put the `{{substitutions}}` and I'll go from there."

Ha.

Point #1: such substitutions are part of a *templating engine*, which the native framework lacks. In this article I'll detail many choices in handling the HTML portion of a component, such as what format to keep it in, how to parameterize it, when to use the `<template>` tag, how to render and then re-render it, and how to know *when* to re-render it.

In these examples I'll be using single-file components with zero dependencies, so no `fetch`ing HTML separately from the `import`ed code. I'll use a little bit of Typescript for documentation purposes. I'll also hold the preference of always using the Shadow DOM, and always keeping the HTML/CSS at the bottom of the code file, not because it is easy but because it is hard.

Our example is a `<flex-row>` component, which merely styles its children in a flexbox's row alignment.  Since it accepts child nodes as input, we'll see the `<slot>` tag instructing the native framework where to put those children.  Example usage: 

```html
<flex-row>
  <div>Left sidebar.</div>
  <div>Main content, containing a very long text that wraps around the viewport multiple times. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>
</flex-row>
```

### Simplest Example

```typescript
customElements.define('flex-row', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }).innerHTML = /*html*/`<slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start;"></slot>`;
  }
})
```

By inlining the `class` definition right inside the final `.define` call, we don't actually need to name the class. `.attachShadow` creates the Shadow DOM, and returns a reference to it which we can immediately `.innerHTML` it with a string which is our initial template.

The `/*html*/` comment is a hint to our IDE that the backtic string should be color-syntax highlighted as HTML.  CSS is inlined but a `<style></style>` after the `</slot>` works just as well. But if our template is large we'd probably move it out of the code to the bottom of the file...

### Put Template At Bottom

```typescript
customElements.define('flex-row', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }).innerHTML = template();
  }
})

function template() {
  return /*html*/`<slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start;"></slot>`;
}
```
Moving the template to the end of the file looks cleaner. But, calling `.define` so early can error with `template() is undefined` if we used `const template = () => ...`.  So here I must use `function` so hoisting happens.  Or I could name the class and move just the `.define` call to the very bottom... where it's easy to forget it.

Anyway, let's add a parameter `<flex-box wrap='nowrap'>` for our template to wrangle.

### Add Parameter for `flex-wrap`

```typescript
customElements.define('flex-row', class extends HTMLElement {
  connectedCallback() {
    const wrap = this.getAttribute('wrap');
    this.attachShadow({ mode: 'open' }).innerHTML = template(wrap);
  }
);

function template(flexWrap: string) {
  return /*html*/`<slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start; ${flexWrap ? `flex-wrap: ${flexWrap};` : ''}"></slot>`;
}
```
One difference in attaching the Shadow in `connectedCallback` instead of the `constructor` is that the element's attributes are ready.  Our template function is parameterized like any function.

Now, using this feature is weird. Why `<flex-box wrap='nowrap'>` instead of just `<flex-box nowrap>` or `<flex-box wrap>`?  It also never re-renders.  Let's add both of these features.

### Render Method and Property Watching

```typescript
customElements.define('flex-row', class FlexRow extends HTMLElement {
  connectedCallback() {
    this.wrap = typeof this.getAttribute('wrap') === 'string' ? 'wrap' : typeof this.getAttribute('nowrap') === 'string' ? 'nowrap' : '';
    this.attachShadow({ mode: 'open' }).innerHTML = template(this.wrap);
  }
	
  #wrap = '';
  get wrap() { return this.#wrap; }
  set wrap(v) { this.#wrap = v; FlexRow.render(this); };

  static render({ shadowRoot, wrap }: FlexRow) {
    if (shadowRoot) shadowRoot.innerHTML = template(wrap);
  }
});

function template(flexWrap: string) {
  return /*html*/`<slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start; ${flexWrap ? `flex-wrap: ${flexWrap};` : ''}"></slot>`;
}
```
There's a lot to unpack here. 

1) We check both attributes `wrap` and `nowrap` on init.
2) We store the appropriate string in a javascript private variable `this.#wrap`. (Typescript shims this since browser support isn't yet complete, but my later examples won't use it anyway.)
3) We create a getter/setter pair so when outside code changes the property we can react.
4) We declare a *static* `render` function for the re-render. 

Now the consumer can initialize our component with short attributes like `<flex-row wrap>` and `<flex-row nowrap>` but then change them anytime with the property `ourElement.wrap = 'nowrap';`.

If we didn't make the backing field `#wrap` a private member, then the consumer could just hit it directly with `ourElement._wrap = ...` and be left wondering why nothing works, and why is there both a `wrap` and a `_wrap` property visible in the browser devtools.

Similarly, we hide our render function by making it static. This does require naming the class, but in return we can destructure the parameter to avoid a lot of `this.` prefixes.

Point #2: attributes and properties don't automatically mirror each other in HTML Custom Elements.  Think of the `.value` of `<input type=text value='initialVal' />`: the attribute has the initial value and is effectively a constant, while only the property updates according to the user.  Everything works that way with HTML Custom Elements.  

But what if we wanted some mirroring, especially so CSS could alter itself based on whether the flex-row was currently wrapping or not?  CSS only looks at attributes not properties...

### Render Method and Property Watching II

```typescript
class FlexRow extends HTMLElement {
  connectedCallback() {
    this.wrap = typeof this.getAttribute('wrap') === 'string' ? 'wrap' : typeof this.getAttribute('nowrap') === 'string' ? 'nowrap' : '';
    this.attachShadow({ mode: 'open' }).innerHTML = template(this.wrap);
  }

  get wrap() { return this.getAttribute('wrap'); }
  set wrap(v) { this.setAttribute('wrap', v || ''); render(this); };
}

const render = ({ shadowRoot, wrap }: FlexRow) => shadowRoot ? shadowRoot.innerHTML = template(wrap) : null

const template = (flexWrap: string | null) =>
  /*html*/`<slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start; ${flexWrap ? `flex-wrap: ${flexWrap};` : ''}"></slot>`;

customElements.define('flex-row', FlexRow);
```
A couple of things here.

Since we need to update the attribute whenever the property changes, the attribute might as well *be* the backing field.

For the sake of example I moved `.define` to the very bottom, allowing us to use arrow functions for `template()`.  Since that's a pretty one-liner we'll do `render()` the same way as an alternative to the static. 

The new features come with a couple of issues.

The `nowrap` attribute lacks a matching property, and the `wrap` property accepts the string "nowrap", which can confuse our users. I'll defer on this design issue since it's unrelated to templates but did want to call it out.

More importantly, later updates to the attributes via `ourElement.attributes['wrap'] = 'wrap'` or `ourElement.setAttribute('wrap','wrap')` will do nothing, even though the attributes worked initially, potentially disappointing our users. 

### Property AND Attribute Watching

```typescript
class FlexRow extends HTMLElement {
  connectedCallback() {
    this.wrap = typeof this.getAttribute('wrap') === 'string' ? 'wrap' : typeof this.getAttribute('nowrap') === 'string' ? 'nowrap' : '';
    this.attachShadow({ mode: 'open' }).innerHTML = template(this.wrap);
  }

  get wrap() { return this.getAttribute('wrap'); }
  set wrap(v) { this.setAttribute('wrap', v || ''); render(this); };

  static observedAttributes = ['wrap', 'nowrap'];

  attributeChangedCallback(name: string, oldValue: string, newValue: string) {
    if (oldValue !== newValue) render(this);
  }
}

const render = ({ shadowRoot, wrap }: FlexRow) => shadowRoot ? shadowRoot.innerHTML = template(wrap) : null

const template = (flexWrap: string | null) =>
  /*html*/`<slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start; ${flexWrap ? `flex-wrap: ${flexWrap};` : ''}"></slot>`;

customElements.define('flex-row', FlexRow);
```

Adding `attributeChangedCallback`, and listing the possible values for its first argument in the static (and required) `observedAttributes` allows us to react to attribute changes. Normally we need a `this[name] = newValue;` in that function to copy the value from the attribute to the property, but since our property uses a getter/setter already pointed to the attribute, there's no need.  We of course must check for infinite loop.

I said at the start of this article there's no simple substitutions like in React and Angular.  We've been using template string literals and `.innerHTML` to parse the string for us.  This is fine if the component is never re-rendered, being both permanent and one-use, like a main-menu bar.  But when you introduce re-rendering, the re-parsing of strings has a hefty performance hit. 

Let's fix our performance problem with a single string parse for the initial render, and a more surgical update for re-rendering. We'll need the `<template>` HTML tag and the `.cloneNode` method for this one.  

### Precompile Template, Minimal Re-render

```typescript
const template = /*html*/ `
<template>
    <slot style="display: flex; flex-direction: row; flex: 1 1 auto; align-items: flex-start;"></slot>
</template>`;

let fragment: DocumentFragment;

class FlexRow extends HTMLElement {
  static observedAttributes = ["wrap", "reverse"] as const;

  #elementsCache: {
    rootElement?: CSSStyleDeclaration;
  } = {};

  connectedCallback() {
    if (!fragment) fragment = new DOMParser().parseFromString(template, "text/html").head.querySelector("template")!.content;
    const myFragment = fragment.cloneNode(true) as DocumentFragment;
    this.#elementsCache.rootElement = (myFragment.firstElementChild as HTMLElement).style;
    for (const attribute of FlexRow.observedAttributes) this.attributeChangedCallback(attribute, "", this.getAttribute(attribute));
    this.attachShadow({ mode: "closed" }).appendChild(myFragment);
  }

  // reflect properties to attributes
  get wrap() {
    return this.getAttribute("wrap");
  }
  set wrap(value: string | null) {
    this.setAttribute("wrap", value || "");
  }

  get reverse() {
    return this.getAttribute("reverse");
  }
  set reverse(value: string | null) {
    if (value == null) this.removeAttribute("reverse");
    else this.setAttribute("reverse", value);
  }

  // reflect attributes to properties
  attributeChangedCallback(name: (typeof FlexRow.observedAttributes)[number], oldValue: string | null, newValue: string | null) {
    if (oldValue === newValue) return; // prevent infinite loop
    //console.log("name", name, "old", oldValue, "new", newValue);
    switch (name) {
      case "wrap":
        this.#elementsCache.rootElement!.setProperty(name, newValue);
        return;
      case "reverse":
        // newValue = null means attr is now missing;  newValue == "" means attribute there
        this.#elementsCache.rootElement!.setProperty("flex-direction", newValue == "" || (!!newValue && newValue != "false") ? "row-reverse" : "row");
        return;
    }
  }
}

customElements.define("flex-row", FlexRow);
```

Our `template()` function is now just a variable no longer accepting parameters because it's only going to be used once.  Its contents are wrapped in a `<template>` tag because now the DOMParser parses the text and returns the disconnected node tree. We then immediately querySelector it for the `<template>` tag, and put the `content` into a variable for use by every instance of `<flex-row>` afterward. It will never be re-parsed again.  

Given the template that this particular instance of flex-row will use, we then find and cache each element or piece that changes at runtime in response to attribute/property changes. Here it's just the `.style` of the root element.  

When a property changes, we reflect its new value into the corresponding attribute. When an attribute changes, the standard `attributeChangedCallback` is called.  It's here where we update our component's HTML.  We also call this from a loop in `connectedCallback` on initialization to remove all dummy values that were cloned from the static template.

Note that if `oldValue` or `newValue` are null, this means the attribute didn't exist at all, which is a distinct case from it existing but with an empty string. 

This way is a bit more typing and complexity, but on re-render, that sole `setProperty` is astronomically faster than creating and parsing strings, especially if said string invokes other custom components.