An Introduction to Frameworkless Web Components

This tutorial provides an introduction to standard web components written without using a JavaScript framework. You’ll learn what they are and how to adopt them for your own web projects. A reasonable knowledge of HTML5, CSS, and JavaScript is necessary.

What are Web Components?

Ideally, your development project should use simple, independent modules. Each should have a clear single responsibility. The code is encapsulated: it’s only necessary to know what will be output given a set of input parameters. Other developers shouldn’t need to examine the implementation (unless there’s a bug, of course).

Most languages encourage use of modules and reusable code, but browser development requires a mix of HTML, CSS, and JavaScript to render content, styles, and functionality. Related code can be split across multiple files and may conflict in unexpected ways.

JavaScript frameworks and libraries such as React, Vue, Svelte, and Angular have alleviated some of the headaches by introducing their own componentized methods. Related HTML, CSS, and JavaScript can be combined into one file. Unfortunately:

  • it’s another thing to learn
  • frameworks evolve and updates often incur code refactoring or even rewriting
  • a component written in one framework won’t usually work in another, and
  • frameworks can be heavy and limited by what is achievable in JavaScript

A decade ago, many of the concepts introduced by jQuery were added to browsers (such as querySelector, closest, classList, and so on). Today, vendors are implementing web components that work natively in the browser without a framework.

It’s taken some time. Alex Russell made the initial proposal in 2011. Google’s (polyfill) Polymer framework arrived in 2013, but three years passed before native implementations appeared in Chrome and Safari. There were some fraught negotiations, but Firefox added support in 2018, followed by Edge (Chromium version) in 2020.

How do Web Components Work?

Consider the HTML5 <video> and <audio> elements, which allow users to play, pause, rewind, and fast-forward media using a series of internal buttons and controls. By default, the browser handles the layout, styling, and functionality.

Web components allow you to add your own HTML custom elements — such as a <word-count> tag to count the number of words in the page. The element name must contain a hyphen (-) to guarantee it will never clash with an official HTML element.

An ES2015 JavaScript class is then registered for your custom element. It can append DOM elements such as buttons, headings, paragraphs, etc. To ensure these can’t conflict with the rest of the page, you can attach them to an internal Shadow DOM that has it’s own scoped styles. You can think of it like a mini <iframe>, although CSS properties such as fonts and colors are inherited through the cascade.

Finally, you can append content to your Shadow DOM with reusable HTML templates, which offer some configuration via <slot> tags.

Standard web components are rudimentary when compared with frameworks. They don’t include functionality such as data-binding and state management, but web components have some compelling advantages:

  • they’re lightweight and fast
  • they can implement functionality that’s impossible in JavaScript alone (such as the Shadow DOM)
  • they work inside any JavaScript framework
  • they’ll be supported for years — if not decades

Your First Web Component

To get started, add a <hello-world></hello-world> element to any web page. (The closing tag is essential: you can’t define a self-closing <hello-world /> tag.)

Create a script file named hello-world.js and load it from the same HTML page (ES modules are automatically deferred, so it can be placed anywhere — but the earlier in the page, the better):

<script type="module" src="./hello-world.js"></script> <hello-world></hello-world> 

Create a HelloWorld class in your script file:

// <hello-world> Web Component class HelloWorld extends HTMLElement { connectedCallback() { this.textContent = 'Hello, World!'; } } 

Web components must extend the HTMLElement interface, which implements the default properties and methods of every HTML element.

Note: Firefox can extend specific elements such as HTMLImageElement and HTMLButtonElement. However, these don’t support the Shadow DOM, and this practice isn’t supported in other browsers.

The browser runs the connectedCallback() method whenever the custom element is added to a document. In this case, it changes the inner text. (A Shadow DOM isn’t used.)

The class must be registered with your custom element in the CustomElementRegistry:

// register <hello-world> with the HelloWorld class customElements.define( 'hello-world', HelloWorld ); 

Load the page and “Hello World” appears. The new element can be styled in CSS using a hello-world { ... } selector:

See the Pen <hello-world> component by SitePoint (@SitePoint)
on CodePen.

Create a <word-count> Component

A <word-count> component is more sophisticated. This example can generate the number of words or the number of minutes to read the article. The Internationalization API can be used to output numbers in the correct format.

The following element attributes can be added:

  • round="N": rounds the number of words to the nearest N (default 10)
  • minutes: shows reading minutes rather than a word count (default false)
  • wpm="M": the number of words a person can read per minute (default 200)
  • locale="L": the locale, such as en-US or fr-FR (default from <html> lang attribute, or en-US when not available)

Any number of <word-count> elements can be added to a page. For example:

<p> This article has <word-count round="100"></word-count> words, and takes <word-count minutes></word-count> minutes to read. </p> 

WordCount Constructor

A new WordCount class is created in a JavaScript module named word-count.js:

class WordCount extends HTMLElement { // cached word count static words = 0; constructor() { super(); // defaults this.locale = document.documentElement.getAttribute('lang') || 'en-US'; this.round = 10; this.wpm = 200; this.minutes = false; // attach shadow DOM this.shadow = this.attachShadow({ mode: 'open' }); } 

The static words property stores a count of the number of words in the page. This is calculated once and reused so other <word-count> elements don’t need to repeat the work.

The constructor() function is run when each object is created. It must call the super() method to initialize the parent HTMLElement and can then set other defaults as necessary.

Attach a Shadow DOM

The constructor also defines a Shadow DOM with attachShadow() and stores a reference in the shadow property so it can be used at any time.

The mode can be set to:

  • "open": JavaScript in the outer page can access the Shadow DOM using Element.shadowRoot, or
  • "closed": the Shadow DOM is inaccessible to the outer page

This component appends plain text, and outside modifications aren’t critical. Using open is adequate so other JavaScript on the page can query the content. For example:

const wordCount = document.querySelector('word-count').shadowRoot.textContent; 

Observing WordCount Attributes

Any number of attributes can be added to this Web Component, but it’s only concerned with the four listed above. A static observedAttributes() property returns an array of properties to observe:

 // component attributes static get observedAttributes() { return ['locale', 'round', 'minutes', 'wpm']; } 

An attributeChangedCallback() method is invoked when any of these attributes is set in HTML or JavaScript’s .setAttribute() method. It’s passed the property name, the previous value, and new value:

 // attribute change attributeChangedCallback(property, oldValue, newValue) { // update property if (oldValue === newValue) return; this[property] = newValue || 1; // update existing if (WordCount.words) this.updateCount(); } 

The this.updateCount(); call renders the component so it can be re-run if an attribute is changed after it’s displayed for the first time.

WordCount Rendering

The connectedCallback() method is invoked when the Web Component is appended to a Document Object Model. It should run any required rendering:

 // connect component connectedCallback() { this.updateCount(); } 

Two other functions are available during the lifecycle of the Web Component, although they’re not necessary here:

  • disconnectedCallback(): called when the Web Component is removed from a Document Object Model. It could clean up state, aborting Ajax requests, etc.
  • adoptedCallback(): called when a Web Component is moved from one document to another. I’ve never found a use for it!

The updateCount() method calculates the word count if that’s not been done before. It uses the content of the first <main> tag or the page <body> when that’s not available:

 // update count message updateCount() { if (!WordCount.words) { // get root <main> or </body> let element = document.getElementsByTagName('main'); element = element.length ? element[0] : document.body; // do word count WordCount.words = element.textContent.trim().replace(/s+/g, ' ').split(' ').length; } 

It then updates the Shadow DOM with the word count or minute count (if the minutes attribute is set):

 // locale const localeNum = new Intl.NumberFormat( this.locale ); // output word or minute count this.shadow.textContent = localeNum.format( this.minutes ? Math.ceil( WordCount.words / this.wpm ) : Math.ceil( WordCount.words / this.round ) * this.round ); } } // end of class 

The Web Component class is then registered:

// register component window.customElements.define( 'word-count', WordCount ); 

See the Pen <word-count> component by SitePoint (@SitePoint)
on CodePen.

Continue reading An Introduction to Frameworkless Web Components on SitePoint.

Similar Posts