einarvalur.co

HTML 2 template

I've been trying out WebComponents for the last few weeks. They are a handy little API that allows you to write complex UI without the need for frameworks/libraries.

They aren't problem-free and one of the more annoying thing with them is, where to put the template or the markup that makes up the Components.

I played with Polymer when it was starting out and one of the things proposed there was to use the <link /> tag to do imports or linking. That was (IMHO) a really good idea.

The <link /> tag has been used for years to link CSS resources to documents, so using it to link WebComponents made perfect sense. A WebComponent can be defined within a \*.html document:

<template id="component-template">
    <div>
        ...html content goes here
    <div>
</template>
<script>
    class Component extends HTMLElement {
        constructor() {
            super();
            this.appendChild(document.getElementById('component-template').content.cloneNode(true));
        }
    }
    customComponent.define('x-component', Component);
<script>

and then link it to the document like this:

<html>
    <head>
        <link rel="document" href="/component.html" />
    <head>
    <body>
        <x-component></x-component>
    <body>
</html>

There has been a lot of progress in the world of good ol' JavaScript for the past decade or so. JS was never designed with modular system in mind, but as the libraries/frameworks grew, so did the demand for some sort of modular import system.

At the same time as the pressure of getting import / export into JavaScript, the Polymer team was toying with the <link /> idea. But imports won and we only have the option of importing our code via a js file.

That doesn't leave us with a lot of options when defining WebComponents. After all, JavaScript doesn't understand HTML, so HTML has no business inside a *.js file.

Template strings seem to be the solution. The option is to either define a template tag and add a string through the innerHTML setter:

const template = document.createElement('template');
template.innerHTML = `
    <div>
        ...html content goes here
    <div>
`;

class Component extends HTMLElement {
    constructor() {
        super();
        this
        this.appendChild(template.content.cloneNode(true));
    }
}
customComponent.define('x-component', Component);

or ditch the template element all together, and embed the string in the constructor directly.

class Component extends HTMLElement {
    constructor() {
        super();
        this
        this.innerHTML = `
            <div>
                ...html content goes here
            <div>
        `;
    }
}
customComponent.define('x-component', Component);

But, however one turns, the HTML is always a string. Your IDE will not know that this is actually HTML and not just a random string. Sure, it is probable no big deal to write a plug-in for VSCode or Sublime or Atom or WebStorm or Vim... that can color-code and highlight HTML inside a template literal. The irony is, that all these editors already know HTML, just not when it is used as an embedded language.

A big selling point with WebComponents is that no pre-compilers or bundlers are required (I'm looking at you: Babel -> Webpack). The browser understand modules now via the <string type="module" src="..."> attribute, so every WebComponent can be written as standalone *.js file and imported without any hoop-jumping or name-collision.

...But damn you HTML, where should I put you...

I really wanted to be able to place the HTML into a *.html file and them somehow import it into the WebComponent definition. But import only supports *js files. Not only that but, if your server doesn't provide the correct content-type (as in: application/javascript) the browser will not accept the import, even though it is indeed JS code.

In the end I just gave up and made a tiny plug-in for Babel that converts this:

import template from './some.html';

into

const template = document.createElement('template');
template.innerHTML = `content from ./some.html`;

So I ended up having to pre-compile my code :( I guess it is a small price to pay for code-assistance in my IDE. At least I don't have to bundle my code. If I was happy with using Webpack I could have used raw-loader. It does similar thing. With it I could have written my components like this:

import templateString from './some.html';
const templateElement = document.createElement('template');
templateElement.innerHTML = templateString;

but I just wanted to go that extra mile and have the plug-in create the DOMTemplateElement as well. So now my WebComponents look like this:

import template from './some.html';

window.customElements.define('x-component', class extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: 'open'});
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
});

...and because this file gets imported either via import './some.js' or <script type="module" src="/some.js"></script>, the template variable doesn't get exposed to the global scope and there will not be any collision between component even though it doesn't get bundled up via Webpack's function-scope mechanism.