The Problem
In the web application I’m working on, I’m choosing to take as much care as I can for rending performance. A major part of this decision is to write everything I can from pure JavaScript. This has been challenging, given that I don’t really consider myself a JavaScript developer.
I’ve spent a good amount of the last few weeks trying to understand the “right way” to build interfaces from JavaScript. I spent some time developing a draft application with Vue (because it outperforms React) and, in the process, got a sense of the core functionality I would want and need when building the application. This isn’t the first web application I’ve built, so I wasn’t completely unfamiliar with some things. But this is, by far, the most complex.
In the process, it became clear that the fastest way to render repeated elements on a page (eg: search results) would most likely be to create Web Components from HTML Templates…likely using Slots.Here’s more on Templates and Slots from Mozilla’s Developer Network site.
However… When looking around online on YouTube, various web searches, GitHub, and GitHub Gists, I wasn’t finding anything that showed how to create Web Components, using HTML Templates. What I found instead, was that almsot everyone was generating their templates from HTML enclosed in JavaScript literals. In other words, they weren’t referencing actual HTML templates written directly in HTML code.
For a lot of reasons, I love the work done by Brad at Traversy Media. I can honestly say I wouldn’t be able to do what I’m able to do today without him and his Programming Tutorials on YouTube. Since his is the easiest example for me to find again, here’s an example of what I mean by using `innerHtml` for the Web Component html. Notice the third line of the JavaScript, using “template.innerHTML” :
See the Pen Web Components Example by Brad Traversy (@bradtraversy) on CodePen.
This is how virtually every tutorial I came across does it. But, knowing a thing or two about parsing, I knew that had to be less efficient than generating dom nodes. After various bits of research, I confirmed this, but still couldn’t find an example of how to use a WebComponent, but generate it from JavaScript. If I was better with JavaScript, maybe I’d just “get it,” and maybe this problem is pretty basic for a relatively-decent JavaScript developer. But I wasn’t getting it, and I couldn’t find examples of this.
The Solution
I wanted to be able to use the Web Components directly embedded in HTML, but also generate them via JavaScript by importing the HTML templates (and, thus, avoiding the cost of parsing the template from things like `template.innerHTML`).
Without going through the details of my process, I’ll say that I seem to have stumbled through into a solution that works. Since I can’t find this anywhere else online, I’ve included it below and on GitHub.
- Important Points
- In my example’s `wcElementDetails` class, by default, it assumes it’s being executed via embedded HTML
- In the example I created, I also show the differences in how CSS _can_ be rendered if you generated the component using the ShadowRoot/ShadowDom or not, depending on how you’ve put your CSS together. This is obviously avoidable, but I wanted to include it in the example.
- Why include it in the example? Rendering a Shadow Dom has performance implications.Nolan Lawson tested the performance of style rendering of components with and without a Shadow Dom. You might be inclined to believe, as I was, that using styles in the Shadow Dom would be a performance hit. Turns out, at scale, it’s not. Because there’s a smaller scope to reference for applying styles..
- You simply set the constructor’s first parameter to false to disable this, and give yourself control over the component before choosing where to embed it.
- You’ll notice that <slots> are handled by using `replaceWith()` on the slot’s dom node. It may be possible to get slightly better performance from this by using `.innerHTML` on specific nodes, thereby avoiding the creation of an entirely new node and replacing the pointer.Call it a micro-optimization if you will, but if you’ve got a complex template with lots of data to be relpaced in it, and you’ll be displaying lots of them, it’s worth knowing that.
Finally, for those of you who have trawled the MDN about Web Components, you may notice the code is adapted from their section on using Templates and Slots.Demo
Html
JavaScript
The Code
- GitHub Repo with Code
- Html
- JavaScript
HTML: Here’s the HTML for our Web Component as a <template> Go to this method in the GitHub repo
<template id="element-details-template">
<style>
details {font-family: "Open Sans Light",Helvetica,Arial}
.name {font-weight: bold; color: #217ac0; font-size: 120%}
h4 { margin: 10px 0 -8px 0; }
h4 span { background: #217ac0; padding: 2px 6px 2px 6px }
h4 span { border: 1px solid #cee9f9; border-radius: 4px }
h4 span { color: white }
.attributes { margin-left: 22px; font-size: 90% }
.attributes p { margin-left: 16px; font-style: italic }
</style>
<details>
<summary>
<span>
<code class="name"><<slot name="element-name">NEED NAME</slot>></code>
<i class="desc"><slot name="description">NEED DESCRIPTION</slot></i>
</span>
</summary>
<div class="attributes">
<h4><span>Attributes</span></h4>
<slot name="attributes"><p>None</p></slot>
</div>
</details>
<hr>
</template>
HTML: An example of embedding a rendered template directly into the html. Go to this method in the GitHub repo
<element-details>
<span slot="element-name">slot</span>
<span slot="description">A placeholder inside a web
component that users can fill with their own markup,
with the effect of composing different DOM trees
together.</span>
<dl slot="attributes">
<dt>name</dt>
<dd>The name of the slot.</dd>
</dl>
</element-details>
JavaScript: Note the constructor’s default argument being assumed as true. This is to allow embedding directly into HTML, by assuming it’s generated from embedded HTML.Go to this method in the GitHub repo
constructor(isEmbeddedHtml = true)
{
super();
// laod the element automatically when defined explictly in HTML
// this is the only code that will be executed when embedded in the html
if (isEmbeddedHtml) {
const tpl = document
.getElementById('element-details-template')
.content;
this.attachShadow({mode: 'open'})
.appendChild(tpl.cloneNode(true));
return;
}
//
// everything from here on out executes when the element is
// managed by your own custom javascript.
// ie: it is not generated by embedded html
//
this.el = document
.getElementById('element-details-template')
.content.cloneNode(true);
};
JavaScript: Our `Attach` method is only run if we trigger it from JavaScript. For the sake of this example, I’ve included an argument to use (or not) the Shadow Root.Go to this method in the GitHub repo
Attach(id, withShadowRoot = true)
{
const elContainer = document.createElement("div");
elContainer.appendChild(this.el);
if (withShadowRoot) {
elContainer
.attachShadow({mode: 'open'})
.appendChild(elContainer.cloneNode(true));
}
document.getElementById(id).appendChild(elContainer);
};
JavaScript: Our `SetName` method to modify the component’s “element-name” slot. Notice `replaceWith()`, which could be avoided with a different Web Component structure for the sake of using `.innerHTML` and possibly better performance.Go to this method in the GitHub repo
SetSlotName(name) {
const slot = document.createElement("span");
slot.innerHTML = name;
this.el.querySelector('slot[name="element-name"]').replaceWith(slot);
};
JavaScript: This line is critical, as it assigns our `wcElementDetails` class to our custom `<element-details>` html tags.Go to this method in the GitHub repo
customElements.define('element-details', wcElementDetails);
JavaScript: An example of how to instantiate the wcElementDetails class, modify it, then attach it to some part of your HTML. Go to this method in the GitHub repo
function genWithShadowDom(attachToId, name, description, attributes) {
const el = new wcElementDetails(false);
el.SetSlotName(name);
el.SetSlotDescription(description);
el.SetSlotAttributes(attributes);
el.Attach(attachToId, true);
}
HTML: And, finally, how we can generate and append a component to our `attach-here` element. Go to this method in the GitHub repo
<button onclick="genWithShadowDom('attach-here', 'testName', 'testDescription', 'testAttributes')">with ShadowDom</button>
<button onclick="genWithOutShadowDom('attach-here', 'testName', 'testDescription', 'testAttributes')">withOut ShadowDom</button>
<br><br>
<div id="attach-here"></div>