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.