Introduction
This article is all about WebComponents, a new HTML specification for a component-based application architecture on the web. We’ll first take a brief look at the history of web development and then explore what a component-based architecture actually means.
We’ll explore how the WebComponent specification describes a new way for browsers to implement this architecture without needing framework support, and then ways in which existing frameworks can leverage this support to provide an enhanced experience for developers. Along the way we’ll take a detailed look at the actual specification as well as current support by some actual frameworks.
Finally, we’ll see what still needs to be done, as well as some speculation about the future of this emerging standard.
A brief history of web programming
Component-based architecture
A component-based architecture uses the concept of a “component” as the building block of an application. A component is a reusable element that should have one well-defined role. This is nothing new, for example HTML is full of components: headers, paragraphs, line breaks, images, etc. What is new however — at least for the web — is the idea of allowing developers to create their own components. For example, we could create a UserAvatar
component which is responsible for displaying a user’s profile photo, their name, and providing a link to their profile page. This component should encapsulate all of the necessary mark-up and styles for displaying this content as well as events for interacting with is, such as clicking a link within it. Once defined, this component can then be used anywhere throughout the app.
Components should also be able to contain child components, just like most regular HTML elements, in order to create a component hierarchy. Using a Flux architecture, data is passed down from parent to child via component attributes (sometimes known as “props”) and events should bubble back up when triggered.
It should be no surprise that the most popular frameworks currently provide this component model. Currently, three of the most popular frameworks are Angular, React.js and Vue.js. It’s important to note however that these frameworks offer, some to lesser or greater extents, more functionality over just the component model.
Aside: imperative versus declarative APIs
A recent trend amongst frameworks has been the move from imperative to declarative APIs. But what does this mean exactly? Consider the task of adding up a list of numbers. An imperative program would define precisely how this operation should be performed, for example: –
- Initialise a counter variable “c” to the length of the list
- Initialise a “sum” variable to 0
- Start looping
- Get number from list at index “c”
- Add this number to “sum” and update “sum” with the new value
- Decrement “c”
- Is “c” now 0?
- If so, break from the loop.
- If not, jump back to the start of the loop
At the end of this process, “sum” contains the sum of all values in the list. A declarative API on the other hand provides abstract ways of performing the same operation, such as something like this: –
- Iterate the list and apply the sum() method to the iterator.
The conceptual difference here is that with an imperative API we need to instruct the computer how to perform a task, and with a declarative API we only need to tell it what to do. With a declarative approach, we delegate the how responsibility to the library itself, trusting that it knows a good way of instructing the computer. This has a few advantages: –
- We are not reinventing the wheel each time; looping over a list is a common task so why should we write each step every time?
- Generalisation: the library can make sure that the same code can work over all iterable things, whereas our imperative code only works over a list.
- Perhaps the library realises that this iteration can be done in parallel and does so, whereas our imperative code is doomed to run serially on one processor.
Most modern web frameworks use the declarative approach. A good example of this shift is the move from jQuery — which itself uses an imperative API — to a more modern framework such as React.js.
Why WebComponents?
As mentioned above, each of the component-based frameworks are reinventing the wheel when it comes to the component model. While each framework offers something different on top of this, they all implement the basic component model in largely the same way. This is not such a good thing as improvements in one will not directly affect another, and of course there’s a lot of duplication of effort between them.
Due to this separation of implementations, a “component” is not really a singular concept; a React.js component is very different from an Angular component for example in its implementation. This means that a component written in React could not easily be added to an Angular application as Angular has no method of creating and using this React component itself.
The WebComponent custom element specification therefore takes this concept of a component and removes it from the framework, instead placing the implementation within the browser. This has a number of advantages: –
- A browser can make its own implementation decisions so long as it follows the specifications. This means that each browser can optimise its WebComponents implementation however it likes, meaning that it should be much faster and efficient than framework-based implementations.
- As the implementation now exists within the browser, frameworks themselves can become more lightweight as they only need to provide a thin wrapper around the WebComponent specifications.
- Frameworks currently use tricks in order to encapsulate styles within components. With the Shadow DOM specification, this again will be handled by the browser meaning that frameworks will not need to provide any workarounds.
Another important benefit comes from improved semantics.
Semantics
When discussing semantics on the web, we usually refer to how the meaning of information can be portrayed by the markup. For example, imagine a list of films. Currently this list might be contained within an OL element (ordered list), with each film inhabiting an LI (list item) element. So far so good. Within each LI, the film’s title, production year, director name and summary need to be displayed. We might choose to display the title in a header element (H1, H2, etc.), the director’s name and production year in SPANs, and the summary in a P (paragraph) element.
Combined with relevant CSS styles, this list of movies can be made to look good to users. However, the semantics of this list are not as clear as they could be. Imagine that we want to take this list of films and analyse it, perhaps adding the items to a database. The list itself is clearly defined in the markup (OL with LI children) but after this things can become a little more complicated. The film title is marked fairly well by the header tag, but the rest of the information is not so well defined. The page designer decided to place the director’s name and production year in two SPAN elements, however there’s nothing about those elements that tells us specifically what the text within means semantically. For example, a SPAN containing “1983” could be a year, but could also be a rating of some kind or a year representing a different time (e.g. home video release date). The director’s name could be interpreted as the lead actor’s name for example.
It’s common to attach classes to elements for styling and sometimes semantic purposes. For example, the SPAN containing the director’s name could have the class “director-name”. This certainly helps, however this is not always the case; sometimes classes are only used for styling such as “smaller-bold”, which does not provide any semantic meaning.
Now consider the same list using custom elements. The OL could be replaced by a tag called “film-list”. The LI could be “film”. The title could be “film-title”, and so on. When viewing this page, users would see no difference, however when analysed the semantics are a lot clearer. The information contained within each custom elements is now given a much greater meaning simply by the use of these custom elements.
It’s important to note that frameworks generally remove this semantic information as part of the processing of the page. While custom elements can be defined in the framework, the rendering pipeline reduces these custom elements back to standard HTML elements, meaning that semantic information is lost.
WebComponent specs
The main WebComponent specification is split into a number of smaller specifications, all of which need to be implemented by a browser in order for it to claim full compatibility.
Browser support is improving all the time and as WebComponents are becoming a web standard we can expect support to improve with each new version. For browsers that don’t yet support one or more of the specifications, polyfills exist which allow them to use WebComponents, albeit with reduced performance.
Custom Elements
The custom elements specification is the real focus of this article as it is the specification that allows developers to define custom HTML elements without the need of a framework.
Custom elements are defined using a JavaScript API (a sub-class of the HTMLElement
base class) which allows a developer to specify how the element should be rendered and how it should behave, based on input attributes and events. Once defined, those elements can then be used like any other regular HTML element within the rest of the application. The definitions can of course then be utilised in other applications too, as well as in applications that use different frameworks. As an example of this, check out WebComponents.org for a library of components that are ready to use.
Shadow DOM
Shadow DOM is an important specification that allows custom elements to exist. The regular DOM (Document Object Model) is created by the browser when displaying a page and is a hierarchical structure containing all of the page elements. The Shadow DOM encapsulates a custom element’s internal element structure within the element, allowing a component to separate its own DOM from the rest of the document. This becomes important for re-usability and so that components can look and work correctly regardless of their containing page, so that event handlers, styles and other DOM attributes do not seep through when not wanted.
Styling
One of the benefits of Shadow DOM is the encapsulation of a component’s styles. As mentioned above, existing frameworks employ a range of workarounds in order to implement this encapsulation in their own components, however Shadow DOM is implemented directly by the browser and is therefore more efficient causes workarounds to become superfluous.
The problem currently comes from an inherent property of CSS, namely the ‘C’ in ‘CSS’: “cascading”. Styles defined in CSS, by design, cascade down to child components. A DIV element with a “color” attribute of “green” causes contained text to be displayed in green. This style then cascades to any other element contained within the DIV, unless that element explicitly overrides the “color” attribute. This is of course desirable in most cases and is what gives CSS its power, however for custom elements we sometimes do not want page styles to affect the component styles. For example, a custom element might want to inherit the parent “color” attribute, but not the padding and margin which would prevent the custom element from being displayed correctly.
HTML Templates and Slots
The WebComponents specification also introduces the template and slot elements to HTML. Both of these new elements allow re-use of markup by building a template which can then be used within a custom element.
A template is a container which can contain any other elements and which can be given a specific ID. These templates can then be inserted into a custom element by ID, and the browser will automatically insert all inner elements automatically. This allows common markup to be reused easily without duplication.
A template on its own can only contain static content; to display dynamic content the slot element is required. Slots can be inserted into a template and are referenced by name and can also contain default content. After defining a slot element within a template, elements can be inserted into that slot (known as slotting an element) by inserting the element to be slotted inside the custom element itself and setting the element’s slot attribute to the name of the desired slot.
For example, imagine that a custom element called my-component has a slot defined with the name username. The slot can be filled with a child element like so: –
Admin
When rendering the above element, the browser would replace the username slot element in the template with the span element.
Comparing this to React for example, this is very similar to the this.props.children system.
For more details of templates and slots, see this MDN article.
Libraries
While WebComponents can be used without any library support currently, there are reasons why the use of a WebComponents library might make sense: –
- Different API: as mentioned, the WebComponents specification defines an imperative API which has disadvantages as listed earlier. A library could build upon this imperative API and provide its own declarative API which might be easier for developers to use, as well as offering other benefits.
- Polyfills: where browsers lack support for certain features, the library can implement those features in a polyfill, which is a pure JavaScript implementation of that feature. While not as good as actual browser support, these polyfills allow WebComponents to work to the level required by the library.
A few libraries currently exist for using WebComponents, two of which will be discussed here. For more details, check out the Libraries page on WebComponents.org.
Polymer.js and LitElement
Polymer.js is probably the most well-known WebComponents library and is built by Google.
The implementation of custom elements in Polymer.js uses LitElement
, a new base class for declaring custom elements. This new class provides a declarative API on top of the default HTMLElement
which is provided by the browser, and is a sub-class thereof. So instead of declaring a custom element as a sub-class of HTMLElement
, declaring it as a sub-class of LitElement
instead provides these new features. LitElement also uses lit-html
to provide HTML templates. While LitElement is part of Polymer.js, the class follows the WebComponent standard meaning that LitElements can be used with other frameworks, just as standard HTMLElement-based elements, or without the rest of Polymer.js.
Polmer.js also provides a set of polyfills to make sure that web applications written using Polymer.js can run on browsers without full WebComponents support.
Ionic 4 and Stencil.js
Stencil.js is built by the Ionic team, who are known for building a framework on top of Apache’s Cordova to enable running web apps on mobile devices.
Like Polymer.js, Stencil presents the developer with a declarative API on top of the native WebComponents imperative API. Stencil on the other hand is a compiler which takes components written using this new API and compiles them to native custom elements, which can then be used with any other framework. This means that Stencil.js only requires a very small runtime component, which includes a dynamic loader for polyfills based on the host browser.
Conclusion
To summarise, WebComponents are steadily gaining traction as an alternative to the large number of JavaScript web application frameworks which all solve similar problems. As WebComponents are a standard that all browsers will follow, writing web applications using WebComponents liberates a developer from a specific framework lock-in.
Web application frameworks can still be used if necessary, and indeed frameworks such as Angular.js offer more than is encapsulated by the WebComponents specification. However, re-use of components between frameworks means that less work will have to be re-implemented for each, and frameworks might even get smaller as they will only need to provide additional functionality as necessary.
Existing frameworks might also decide to integrate WebComponents directly instead of implementing their own versions of templating, the component model, etc. Such an example is Angular Elements. Some libraries have stated that their component model is sufficiently different from custom elements meaning that they will continue to use their own implementation, however they will still allow interoperability (React.js). Please check custom-elements-everywhere.com for up-to-date details on framework support for custom elements.
Browsers are implementing more and more of the standard as time goes by. The current state of browser support can be checked here: custom elements, HTML templates and shadow DOM.
The WebComponents specification is an exciting addition to the HTML standards, and one that will surely see increasing use within the growing area of web and progressive web applications.