Web Component technology is, well, it's delicious bits of Web Candy. Crunchy and hard on the outside, soft and chewy on the inside, delightful throughout.
Of course, with any technology, you have to ask: Why?
Short Answer: Encapsulation
Encapsulation basically means bundling together all the stuff you need to get something done, and hiding the details, so you can just use the thing. We've had encapsulation in software development for decades, and now we finally have it for HTML pages, as well.
As a practical example, think about how many times you've written (or, more likely, copy-and-pasted) the HTML, CSS and JavaScript you need to create a modal "loading" spinner. I've done it at least a dozen times that I can recall.
Enough already, where's the code?
The first step in creating my "loading modal" component was identifying the bits I previously copy-pasted, and doing one FINAL copy-paste.
It generally starts with some HTML:
<div id="loading">
<div class="message">
<p>Loading... </p>
</div>
</div>
And corresponding CSS:
#loading {
background-color: gray;
height: 100%;
left: 0;
opacity: 0;
position: fixed;
top: 0;
visibility: hidden;
width: 100%;
z-index: 1;
}
#loading.visible {
opacity: 97%;
visibility: visible;
transition: visibility 0s linear 0s, opacity .25s 0s;
}
#loading .message {
height: 100%;
margin: 20px 2px;
padding: 2px 16px;
position: relative;
text-align: center;
top: 40%;
vertical-align: middle;
}
And we need a little JavaScript to show and hide the #loading
div as needed:
function showLoading() {
document.getElementById('loading').classList.add('visibile')
}
function hideLoading() {
document.getElementById('loading').classList.remove('visibile')
}
First Draft
In order to make that disparate bunch of stuff a single, unified (encapsulated) Web Component, we create a JS file which defines our component:
window.customElements.define('eh-loading-modal',
class EhLoadingModal extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
}
})
There you have a basic - and useless - web component. We use the customElements.define
function to define the HTML tag we'll use to use our component (<eh-loading-modal>
), and the class which will implement the component. We need a constructor (and always call super()
to ensure everything sets up correctly), and we need to attach a "Shadow Root" to our component instance. This is basically a tiny DOM which is private to each instance of our component: each time we use that tag on a page, we'll be creating another copy of the tiny DOM inside the page's big DOM.
Now, what do we put into the tiny DOM? Our HTML and CSS, from above, like this:
window.customElements.define('eh-loading-modal',
class EhLoadingModal extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
this.shadowRoot.innerHTML = `
<style>
#loading {
background-color: gray;
height: 100%;
left: 0;
opacity: 0;
position: fixed;
top: 0;
visibility: hidden;
width: 100%;
z-index: 1;
}
#loading.visible {
opacity: 97%;
visibility: visible;
transition: visibility 0s linear 0s, opacity .25s 0s;
}
#loading .message {
height: 100%;
margin: 20px 2px;
padding: 2px 16px;
position: relative;
text-align: center;
top: 40%;
vertical-align: middle;
}
</style>
<div id="loading">
<div class="message">
<p>Loading... </p>
</div>
</div>
`
}
})
Just copy-paste. Simple. But now we need to be able to show the thing on demand. Remember I said the shadowRoot
is a tiny DOM inside the component. Well, that means that things inside that tiny DOM are encapsulated - we shouldn't try to go mucking about with them directly. We need to add functions to the component which we can call to show and hide the loader. These live inside the component class, and are ALMOST the same as we'd get from copy-pasting the original:
show() {
this.shadowRoot.getElementById('loading').classList.add("visible")
}
hide() {
this.shadowRoot.getElementById('loading').classList.remove("visible")
}
Eureka!
Now, we can build a really simple proof of concept page:
<html>
<head>
<script src="eh-loading-modal.js"></script>
</head>
<body>
<main>some content here</main>
<eh-loading-modal></eh-loading-modal>
<script>
const loader = document.getElementsByTagName('eh-loading-modal')[0]
loader.show()
setTimeout(() => loader.hide(), 5000)
</script>
</body>
</html>
Working, but too simple
We have a very simple component. It works. But, it's not very flexible. In the next article, we'll talk about that.