Hey! This is my first DEV article ever, and I would love feedback on how to improve! This is going to be a series, where in each article, I go over one cool trick to help you out (hopefully).
Web Components are really cool in my opinion! Think of the possibility! A powerful UI component framework, without using a framework!
Sometimes however, they can get inefficient to write. This, for the most part, is due to web components being very unopinionated. There is no state-management, no data-binding, etc. These features are often found in libraries and frameworks such as React or lit-element, because the libraries are abstractions over native JS APIs. On the other hand, Web Components are just APIs that are a part of the spec.
Problem Statement:
Custom Elements are nothing without what they contain (child nodes). Yet in Vanilla JavaScript, creating child nodes are a pain.
Let's say you want to render the following markup to a custom element (where {name}
and {date}
represent placeholders that need to be changed):
<h1>Hello, {name}</h1>
<p>Today is {date}</p>
How would we render this to a vanilla custom element?
Built-in Solutions:
Solution 1: Vanilla DOM API Calls ❌
We can use the native DOM APIs to accomplish this.
class DateName extends HTMLElement {
connectedCallback() {
const h1 = document.createElement("h1");
const p = document.createElement("p");
h1.innerText = `Hello, ${this.dataset.name}`;
p.innerText = `Today is ${this.dataset.date}`;
this.appendChild(h1);
this.appendChild(p);
}
}
customElements.define("date-name", DateName);
This works, but for more complex components, the amount of code needed is very high compared to the HTML code we end up with. Secondly, our code is hard to visualize, and is therefore annoying to debug.
Solution 2: innerHTML
❌
The innerHTML
property allows us to directly modify the HTML code inside the custom element, exactly how we would in an HTML
document.
class DateName extends HTMLElement {
connectedCallback() {
this.innerHTML = (`
<h1>Hello, ${this.dataset.name}</h1>
<p>Today is ${this.dataset.date}</p>
`);
}
}
customElements.define("date-name", DateName);
This solves all of our problems! Right? NO! While the innerHTML
approach is definitely easier to read and write, it makes the browser work much harder, which decreases performance by quite a bit. Additionally, this method is very prone to XSS Attacks (more on that here).
Solution 3: HTML Templates ✔️
HTML Template Element (part of the Web Components spec) is a DOM element, where you can write normal HTML code (markup, inline scripts and styles), but nothing will be applied to the DOM. Better explanation here.
In your HTML:
<template>
<h1>Hello, <slot id="name"></slot></h1>
<p>Today is <slot id="date"></slot></p>
</template>
Your Client-Side JS:
// Template:
const dateNameTemplate = document.querySelector("template");
// Custom Element
class DateName extends HTMLElement {
connectedCallback() {
// Copy the nodes in the template:
const clone = dateNameTemplate.content.cloneNode(true);
// Replace Placeholders:
const name = clone.querySelector("#name").parentNo = this.dataset.name;
clone.querySelector("#date").innerText = this.dataset.date;
// Append Node:
this.appendChild(clone);
}
}
customElements.define("date-name", DateName);
Honestly, using HTML Templates is definitly very concise, and I personally really like this method. However, with more advanced components, the conciseness can go away, especially when there is a lot of different placeholders to replace. Regardless, this solution is effective, efficient, and secure.
Best Built-in Solution:
Out of the built-in solutions, the use of HTML templates is the best. The reasoning behind this is that it provides the perfect balance between performance (at par with the native DOM API method) and conciseness (easy to read, especially with well-commented code). However, all of the above solutions are viable, except for maybe using innerHTML
due to security issues.
For any of the above solutions, the following HTML should work:
<date-name data-name="raghavm" data-date="Jan. 19, 2020"></date-name>
Hope this helped! Stay tuned for the next one :)
Top comments (3)
Couldn't help but notice that the HTML you used with the template is different to the other solutions - the additional
span
elements. Is there any way around this?I realized this problem, but chose to ignore it for the sake of simplicity. I will edit the post to fix this problem. Thanks!
To fix this, I would replace the entire placeholder
<span>
with the text, rather than changing itsinnerText
. I'm sure there are other solutions, so let me know if you find one. ;DAnother, much easier, solution would be to replace the
<span>
elements with<slot>
elements. Although slots were made for use with the Shadow DOM, they aren't limited to that. The purpose of a slot is to be replaced, which means it's unlikely that they will be styled.