Overview
In the first part of this series, we looked at two of the four main concepts behind web components: custom elements and the shadow DOM. That was pretty harsh. If you all agree, I think we deserve to see some easier stuff!
The remaining two concepts are ES Modules and HTML templates.
But before we dive in, let's use some tools to make our experimentation more fun.
Tools
Since you're reading this pretty advanced web development article, I'll assume that you have Node.js installed on your computer. Please create a new folder somewhere on your file system, get in it and run:
npm init # just hit all default options
npm install --save-dev es-dev-server
touch index.html
npx es-dev-server --open --node-resolve
This will install es-dev-server, a slim node package (made by the open-wc community) that creates a local server that will serve the index.html
file we just created in your browser.
Now just open up your favorite text editor and add the following markup in index.html
:
<html>
<head>
<title>Web Components Experiments</title>
</head>
<body>
<style>
/* A little optional style for your eyes */
body {
background-color: #1C1C1C;
color: rgba(255, 255, 255, 0.9);
}
</style>
<h1>Hello, world!</h1>
</body>
</html>
If you refresh, you should see a big "Hello, world!" in your browser. Ok now let's get started!
Where were we
Let's start by add a bunch of <my-cool-div>
components in this page! Add this markup in the <body>
:
<style>
my-cool-div {
width: 100%;
height: 200px;
}
</style>
<my-cool-div>
<p>Oh yeah</p>
</my-cool-div>
<my-cool-div>
<p>Drink this Koolaid</p>
</my-cool-div>
If you refresh now, you'll notice the nodes we created do not center their content, nor do they display a blue line above it. That's because we forgot to define <my-cool-div>
in this browser window! Without the corresponding definition, the browser treats those unknown tags like basic HTML nodes, like <div>
if you like. If we define them later, it will upgrade them to custom elements then. Let's do it! Let's bring in our previous code by copying it into the following script tag at the bottom of the body:
<script>
class MyCoolDivElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = `
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
span {
width: 50%;
height: 4px;
background-color: blue;
}
`
this.shadowRoot.appendChild(style)
const span = document.createElement('span')
this.shadowRoot.appendChild(span)
const slot = document.createElement('slot')
this.shadowRoot.appendChild(slot)
}
}
window.customElements.define('my-cool-div', MyCoolDivElement)
</script>
If you refresh, our cool divs should now look like expected.
ES Modules
Ok nice but I don't want to copy-paste this code in every project I'll ever do, let alone in index.html!
Well friend, the platform has the goods. It's called ECMAScript Modules. You can import/export code from different JS files. This is great for reusability and one of the core concepts behind web components. Let's see how it works:
<!-- index.html -->
<html>
<head>
<title>Web Components Experiments</title>
</head>
<body>
<!-- ... -->
<script type="module" src="my-cool-div.js"></script>
</body>
</html>
//my-cool-div.js
export class MyCoolDivElement extends HTMLElement {
/* ... */
}
window.customElements.define('my-cool-div', MyCoolDivElement)
The main difference between a standard <script>
tag and <script type="module">
tag is that the ran script will be encapsulated and run only once. This means that class MyCoolDivElement
will no longer be a global variable (yay) AND we won't try to define it multiple times in the registry if we import that script multiple times.
Why would we import it multiple times?
Well if we make another web component, for example, that has a <my-cool-div>
in its shadow dom, we would need to import it in its module as a dependency! Example:
// my-cool-section.js
import './my-cool-div.js'
class MyCoolSectionElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const section = document.createElement('section')
this.shadowRoot.appendChild(section)
const myCoolDiv = document.createElement('my-cool-div')
section.appendChild(myCoolDiv)
}
}
window.customElements.define('my-cool-section', MyCoolSectionElement)
Or, if we wanted to make another web component that's a cool div too, but cooler:
// my-cooler-div.js
import { MyCoolDivElement } from './my-cool-div.js'
class MyCoolerDivElement extends MyCoolDivElement {
constructor() {
super()
const coolerStyle = document.createElement('style')
coolerStyle.textContent = `
span {
background-color: white;
}
`
this.shadowRoot.appendChild(coolerStyle)
}
}
window.customElements.define('my-cooler-div', MyCoolerDivElement)
That's right, web components are extendable! They're just classes after all! We won't explore that notion further for now but we'll come back to it. The important thing to remember is that importing ES Modules means they run only once and are scoped (unless you intentionally create a side effect in window
like we do for the custom element registration at the end).
HTML Template
Ok so now we can define a custom element, define its shadow DOM and import it as a module from an external file. What's missing? Nothing. We have all we need to build reusable web components! From now on, everything we're adding to the mix is either to improve performance or developer experience.
Speaking of which, noticed how building the shadow DOM sucks? Creating nodes by hand doesn't make for the best developer experience. Let's try to improve this part a bit with <template>. This element is meant to define a DOM structure from markup, like <div>
, but the browser doesn't stamp its content in the DOM right away, it holds it for reuse. This is useful because it allows you to clone the instanciated template content and stamp the clones in the DOM later, making for better rendering performance. With it, we can write our component like so:
// my-cool-div.js
const template = document.createElement('template')
template.innerHTML = `
<style>
:host {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
span {
width: 50%;
height: 4px;
background-color: blue;
}
</style>
<span></span>
<slot></slot>
`
export class MyCoolDivElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
const templateClone = template.content.cloneNode(true)
this.shadowRoot.appendChild(templateClone)
}
}
customElements.define('my-cool-div', MyCoolDivElement) // we can also drop window here for a bit more conciseness
A bit nicer to read right?
Why declare the template outside of the class?
For performance! Cloning a template instance is way faster than creating all of the shadow DOM nodes one by one like we used to (especially when only parts of it need updating as we'll see). Plus, the template creation will occur only once since this script will run as an ES Module! This is so convenient and performant that it's basically considered part of the web component spec, even though, as we have seen, you can live without it for very simple components like <my-cool-div>
.
But of course a web component can be way more complex than that! Encapsulating a DOM tree and a bunch of styles is just the first step on the path to usefulness. In the final part of this series, we'll see how web components can handle any kind of data and react when it changes. We'll also use this opportunity to delve deeper into the web component lifecycle. Finally, we'll take a look at that Google library I told you about in Part 1, the one that will abtract all of the low-level boilerplate code we just learned and make our lives much more easier!
Top comments (3)
Can web components be defined using factory functions instead of classes?
I have to say I’ve never tried, but I read somewhere that yes it’s possible! Javascript classes are, after all, just syntaxic sugar over the existing prototype model, so if you can define prototypes using a factory function, you can probably add the HTMLElement prototype to your outputted object
For more info on classes vs prototype, this is an excellent article:
medium.com/@parsyval/javascript-pr...