Grow with the browser, learn web component fundamentals.

Emil Møller
6 min readOct 12, 2017

--

Browser ❤ Toolbox

Web components are finally here! It’s been a long while on its way, and while we’ve been waiting, we’ve learned to build reusable components using libraries like ReactJS, so let’s learn how to do it natively.

This article is not about libraries, but to highlight that a native set of tools and API’s have been implemented to build custom reusable HTML components for the web.

All the examples are made with features available in the latest Chrome browser and most features can be polyfilled for unsupported browsers.

Web components is a way to extend HTML

Web components are a result of The Extensible Web Manifesto which is a manifest by developers and browser vendors agreeing on adding new low-level capabilities to the web platform.

With these new capabilities we can add a new custom HTML element with shadowDOM using a simple JavaScript class like this:

// https://codepen.io/emolr/pen/pWVRMm// Javascript
class CustomButton extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<button>
<slot></slot>
</button>
`;
}
};
window.customElements.define('custom-button', CustomButton);// HTML
<custom-button>Button</custom-button>

So now after adding custom-button to the customElement register, it’s available as any other HTML element both in HTML and in JavaScript.

Notes on this example:

  • We are extending HTMLElement, so we need to call super() to call the parent constructor.
  • <slot> are a placeholder inside your component that users can fill with their own markup. Read more about the slot element here.
  • A Custom element name must contain a - hyphen in the name so the browser can differentiate from native elements.

The lifecycles of a custom element

A custom element can define lifecycle hooks for running code on specific times in the element lifetime.

These lifecycles are called custom element reactions:

  • Constructor — Called when an instance of the element is created or updated, read more about the constructor in the spec.
  • connectedCallback — Called when the element is inserted into the DOM.
  • disconnectedCallback — Called when the element is removed from the DOM.
  • attributeChangedCallback(name, oldVal, newVal) — Called when an attribute is added, removed, updated, or replaced. Also called when the element is created. (Only attributes listed in the observedAttributes property will receive this callback).
  • adoptedCallback — Called when the element has been moved into a new document.

We’ll go through how we utilize some of the different callbacks throughout the examples in this article.

Use attributes for states and data

As a web component is a way to extend the HTML, it should also look and feel like a native HTML element, and attributes is a good way to communicate to and from a custom element.

Furthermore, attributes can be mapped to properties and properties to attributes so that states and data will be set both in HTML attributes or properties.

To do this automatically we use getters and setters.

// Getters and setters exampleclass CustomCheckbox extends HTMLElement {
set checked(value) {
if (Boolean(value))
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}

get checked() {
return this.hasAttribute('checked');
}

...
}

If we want to pass data to our custom component we can use the lifecycle hook attributeChangedCallback(name, oldVal, newVal) which listens to attributes listed in a property called observedAttributes .

// Listening to attributes
// https://codepen.io/emolr/pen/NaMpVP?editors=0010
class CustomCheckbox extends HTMLElement {
...

set transitionDuration(value) {
this.setAttribute('transition-duration', value);
}

get transitionDuration() {
return this.getAttribute('transition-duration');
}
static get observedAttributes() {
return ['transition-duration'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'transition-duration') {
// Code to run when attribute changes
}
}
...
}

With this in mind we can create a simple checkbox with an customisable transition duration:

// Simple checkbox with customisable transition
// https://codepen.io/emolr/pen/NaMpVP?editors=0010
// JavaScript
class CustomCheckbox extends HTMLElement {
set checked(value) {
if (Boolean(value))
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}

get checked() {
return this.hasAttribute('checked');
}

set transitionDuration(value) {
this.setAttribute('transition-duration', value);
}

get transitionDuration() {
return this.getAttribute('transition-duration');
}

static get observedAttributes() {
return ['transition-duration'];
}

attributeChangedCallback(name, oldVal, newVal) {
if (name === 'transition-duration') {
this.style.transitionDuration = `${this.transitionDuration}s`;
}
}

constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid black;
background: transparent;
}

:host([checked]) {
background: skyblue;
}
</style>
`;
}

connectedCallback() {
this.addEventListener('click', this._toggleChecked);
}

disconnectedCallback() {
this.removeEventListener('click', this._toggleChecked);
}

_toggleChecked() {
this.checked = !this.checked;
}
}
window.customElements.define('custom-checkbox', CustomCheckbox);

Notes on this example:

  • In the shadowRoot I’m writing css in a <style>, this is encapsulated in this instance of the custom element.
  • In <style> I am using :host {...} this is a way to style the custom element, read more about styling web component here.
  • It’s a good idea to prestyle the element to prevent Flash of unstyled content.
  • We set the transition in attributeChangedCallback so the value will be set every time the attribute changes.

Custom Events in web components

Listening for events on a custom component isn’t any different than listening to events on any other HTML element if it’s set it up in a similar fashion.

To make the custom component fire an event, we‘ll use a custom Event:

// Custom events
// https://codepen.io/emolr/pen/NaMpVP?editors=0010
class CustomCheckbox extends HTMLElement {
...
_toggleChecked() {
this.checked = !this.checked;
this.dispatchEvent(new CustomEvent('change', {
detail: {
checked: this.checked,
},
bubbles: true,
composed: true
}));
}
...
}

To listen for this event, we will listen like we would normally do:

let element = document.querySelector('[custom-checkbox]');element.addEventListener('change', (e) => {
console.log(e.detail.checked)
});

Wait for custom elements dependencies

When making complex components that depend on other components you need to make sure that all other components are defined before rendering.

// https://codepen.io/emolr/pen/GMdMRL?editors=0010
...
connectedCallback() {
window.customElements.whenDefined('custom-list-item')
.then(() => {
this.render();
});
}
...

Instead of inserting the HTML directly in the connectedCallback lifecycle hook, we create a render function that renders the content so it can be triggered when dependencies are defined, and reused when the related attributes are changed.

// https://codepen.io/emolr/pen/GMdMRL?editors=0010// JavaScript
const customListItemTemplate = document.createElement('template');
customListItemTemplate.innerHTML = `
<style>
:host {
display: block;
}
:host::before {
content: "•"
}
</style>
<slot></slot>
`;
class CustomListItem extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(customListItemTemplate.content.cloneNode(true));
}
}
class CustomList extends HTMLElement {
set items(value) {
this.setAttribute(JSON.stringify(value));
}

get items() {
return JSON.parse(this.getAttribute('items'));
}

static get observedAttributes() {
return ['items'];
}

attributeChangedCallback(name, oldVal, newVal) {

// Render again if the data changes
if (name === 'items' && oldVal && newVal) {
this.render();
}
}

constructor() {
super();
this.attachShadow({mode: 'open'});
}

connectedCallback() {
if (!this.items)
return;

// Here we wait for component dependencies to be defined
window.customElements.whenDefined('custom-list-item')
.then(() => {
this.render();
});
}

render() {
this.shadowRoot.innerHTML = '';

const items = this.items.map(item => {
let element = document.createElement('custom-list-item');
element.innerHTML = item.content;
return element;
});

items.forEach(item => {
this.shadowRoot.appendChild(item);
});
}

}
window.customElements.define('custom-list-item', CustomListItem);
window.customElements.define('custom-list', CustomList);
// HTML
<custom-list items='[{"content":"Item #1"},{"content":"Item #2"}]'></custom-list>

Notes on this example:

  • We need to make sure we won’t render the DOM until the data we need
    is available using window.customElements.isDefined().
  • window.customElements.isDefined() returns a promise.
  • When passing data via an attribute we need to pass stringified JSON.
  • The setter and getter make sure to JSON.stringify and JSON.parse so we are sure we always get an object when calling this.items.
  • Here i’m using document.createElement('template') for the innerHTML, this is to parse the template on page load before the component is
    being constructed.

In the examples we have managed to create stateful and stateless components using tools which are already available in Chrome and in near future will be ready in Firefox, Edge and Safari.

Thanks for reading. If you found this article helpful, please recommend it
and share it, and follow me for more articles on web components and UI.

I hope the examples above covers the basics to get started using web components today, and hopefully, I have convinced you to try web components without the use of any frameworks or libraries.

Let me know in the comments what your thoughts are about web components, and let me know if you have feedback.

Meanwhile here is a list of resources I have found useful:

Update,
After i wrote this article, I created a library to make web components called FrameJS.

FrameJS was created just before LitElement was created by Google and was trying to solve many of the same things.

FrameJS tried to take the best of existing libraries like SkateJS extended with decorators to make it simpler to use. It’s fully tested, and I still use it to wrap components written in React or Preact to be consumed as a web component.

If you are interested in learning how it all works, you should check out the source code as it contains a lot of good tricks and tests.

--

--

Emil Møller

Frontend developer / designer. Previously @unity3d, @Gracenote