The Boom – a simple web component

THE BOOM is a simple HTML game, meant to feel the Web Components. The goal is to create a simple HTML element that if you don’t click in time, will BOOM to red. The real perpes is to have the magical powers of the creator and build an atom HTML web-component. Exposing something as simple as writing HTML tag like this:

<the-boom></the-boom>
<the-boom sec="1"></the-boom>
<the-boom sec="2"></the-boom>
<the-boom sec="x"></the-boom>

Starting with a basic component:

Custom element names they can’t be single words, they require a dash to be used in them, so this example uses “the-boom“.

class TheBoom extends HTMLElement {  
constructor() {
super();
}
connectedCallback(){
this.className = "blue";
this.innerHTML = '
<style>
the-boom {
width: 100px;
display: inline-block;
margin: 2px; text-align:center;}
</style>
<span>:-)</span>';
}
}
window.customElements.define('the-boom', TheBoom);

To define a custom HTML element we need to parts: the class, extention of HTMLElement and the registration: window.customElements.define.

The connectedCallback is one of the lifecycle events that is executed the element has been inserted into the DOM. The available lifecycle events are:

  • connectedCallback()
  • disconnectedCallback()
  • adoptedCallback()
  • attributeChangedCallback(name, oldValue, newValue)

Now, after understanding the basics, it’s time to build the countdown:

class TheBoom extends HTMLElement {  
constructor() {
super();
this._endOfTime = 0;
}
connectedCallback(){
this.innerHTML = `
<style>
the-boom { width: 100px; display: inline-block; margin: 2px; text-align:center; }
  </style>
<span>:-)</span>;
if(this.hasAttribute('sec') && !isNaN(this.getAttribute('sec'))) {
this._endOfTime = Math.round((new Date()).getTime()/1000) + parseInt(this.getAttribute('sec'));
this.resetTime();
}
}

resetTime(){
let now = Math.round((new Date()).getTime()/1000);
if(this._endOfTime){
if(this._endOfTime && this._endOfTime > now){
this.getElementsByTagName('span')[0].innerText = this._endOfTime - now;
setTimeout(this.resetTime.bind(this),1000)
}else{
this.getElementsByTagName('span')[0].innerText = "Boom";
}
}
}
}
window.customElements.define('the-boom', TheBoom);

Most use cases of the “bind” method where vanity tricks. But this time, without it we lose the scope, the “this” and all the local variables.

Next part – add the click event:

class TheBoom extends HTMLElement {  
constructor() {
super();
this._endOfTime = 0;
this._click = () => {
this._endOfTime = 0;
this.removeEventListener('click',this._click);
this.getElementsByTagName('span')[0].innerText = '('+this.innerText+')';
}

}
connectedCallback(){
this.innerHTML = ...;
if(this.hasAttribute('sec') && !isNaN(this.getAttribute('sec'))) {
this.addEventListener("click", this._click);
this._endOfTime = Math.round((new Date()).getTime()/1000) + parseInt(this.getAttribute('sec'));
this.resetTime();
}
}
resetTime(){
let now = Math.round((new Date()).getTime()/1000);
if(this._endOfTime){
if(this._endOfTime && this._endOfTime > now){
this.getElementsByTagName('span')[0].innerText = this._endOfTime - now;
setTimeout(this.resetTime.bind(this),1000)
}else{
this.getElementsByTagName('span')[0].innerText = "Boom";
this.removeEventListener('click',this._click);
}
}
}
}
window.customElements.define('the-boom', TheBoom);

As you can see, I used the arrow function for the click. Not just for the fun of it, as on the “bind” method, to keep the scope of the element. Without that all the inner variables will be undefined.

Last part – adding colors:


class TheBoom extends HTMLElement {
constructor() {
super();
this._endOfTime = 0;
this._click = () => {
this._endOfTime = 0;
this.className = "green";
this.removeEventListener('click',this._click);
this.getElementsByTagName('span')[0].innerText = this.innerText;
}
}
connectedCallback(){
this.className = "blue";
this.innerHTML = '
<style>
the-boom {
color: white;
width: 100px;
display: inline-block;
margin: 2px; text-align:center;}
the-boom.blue{ background: #1E88E5; }
the-boom.red{ background: red; }
the-boom.green{ background: green; }
</style>
<span>:-)</span>';
if(this.hasAttribute('sec') && !isNaN(this.getAttribute('sec'))) {
this.addEventListener("click", this._click);
this._endOfTime = Math.round((new Date()).getTime()/1000) + parseInt(this.getAttribute('sec'));
this.resetTime();
}
}
resetTime(){
let now = Math.round((new Date()).getTime()/1000);
if(this._endOfTime){
if(this._endOfTime && this._endOfTime > now){
this.getElementsByTagName('span')[0].innerText = this._endOfTime - now;
setTimeout(this.resetTime.bind(this),1000)
}else{
this.className = "red";
this.getElementsByTagName('span')[0].innerText = "Boom";
this.removeEventListener('click',this._click);
}
}
}
}
window.customElements.define('the-boom', TheBoom);

You can see it in action here: “The boom“. A small atom block on the HTML that can provide thousend howers of fun ;-/

Next time, maybe, the isolated web component.

Sad but true update: (29/09/2020)

Web Components had so much potential to empower HTML to do more, and make web development more accessible to non-programmers and easier for programmers. Remember how exciting it was every time we got new shiny HTML elements that actually do stuff? Remember how exciting it was to be able to do sliders, color pickers, dialogs, disclosure widgets straight in the HTML, without having to include any widget libraries?

… I just wanted something that is small and works like a normal HTML element. Yet, it proved so hard I ended up writing my own!

The failed promise of Web Components By Lea Verou