Web Components Introduction: Creating Custom HTML Elements in 2018
The Web Components specification is an emerging collection of technologies that allows you to define encapsulated, custom HTML elements in front-end web apps. Web Components allow you to essentially create your own declarative API for defining UIs.
Concepts and use cases
You don't need to be a web developer to appreciate the benefits of the "Don't Repeat Yourself" (DRY) principle. Without realizing it, many people implement this idea to simplify their daily lives. For example, almost everyone consolidates their errands into one trip to save time and gas money (plus preserve some of their own sanity — imagine if you had to return home in between every store you visit!)
In the case of errands, the repetitive task being eliminated is driving all the way from your house to some destination. The benefits of cutting out repetition in the "real world" are immediately obvious, and they're similar when writing code. Writing compartmentalized, reusable code is the best way to maintain code readability, reduce overall application size, and simplify debugging. The advantages of DRY in programming are crucial, especially when writing applications for the web.
Writing compartmentalized, reusable code is the best way to maintain code readability, reduce overall application size, and simplify debugging.
Until recently, encapsulating code in front-end web apps has been difficult, if not impossible. Several tools and technologies have emerged to help developers overcome this challenge, and they power most of the web these days. The first major tool on the scene was jQuery, which has been displaced over time by JavaScript frameworks like Angular, React, and Vue. These frameworks jumped in to heroically solve the DRY problem on the web by providing their own engines for building custom, declarative UI components.
Of course, the implementation of JavaScript frameworks doesn't come free of charge. If you're building a relatively simple web app or your main focus is simply custom declarative syntax, frameworks come with significant overhead in the form of JavaScript bundle size. Users must download not only your custom component code, but also the code for the framework's rendering engine and other utilities. If you're targeting emerging markets, this is an important consideration.
Even though the utility of frameworks is undeniable, this pitfall is an area in which Web Components can help. The primary use case for Web Components is creating powerful, versatile custom elements powered by compartmentalized JavaScript that run natively in the browser. The user of a Web Components-based app doesn't have to download any engine, because it's already running right on their machine!
Core technologies of Web Components
The Web Components specification is actually a collection of browser APIs that can be grouped into four major categories:
- Custom Elements — JavaScript APIs provided by the browser that let you hook up code (typically a JavaScript class) with custom declarative elements in markup (e.g. <my-custom-element>).
- Shadow DOM — Another set of JavaScript APIs for embedding a "shadow" DOM tree that is rendered separately from the main DOM. You'll usually create a Shadow DOM instance for each custom element which will allow you to scope styles and DOM nodes on a per-element basis. This is crucial to encapsulating elements, as it prevents both style and DOM reference collisions.
- HTML Templates — Specialized DOM elements <template> and <slot> that allow you to create reusable base markup for a custom element with pieces that are overridable on a per-instance basis. (Note that these elements aren't rendered, so you can define them anywhere in your document.)
- HTML Imports — This technology provides a way to encapsulate and reuse the base markup for custom elements. (This would typically work by storing an HTML template in its own file and importing it into an actual page.) This is the most contended piece of the Web Components spec, so be sure to check browser support before using it.
Custom elements: A window into developer-specified markup
While the latter three API categories mentioned above are designed to give Web Components more power, flexibility and development convenience, the custom elements interface is what actually enables Web Components to exist as custom HTML elements. Most of the APIs available for interacting with custom HTML components are available via the CustomElementRegistry abstraction.
Essentially, the browser keeps a running list of custom HTML elements that you specify based on their name and the code that enables their functionality. It's worth noting that these are the two primary components that make up any Web Component. Registration in the CustomElementRegistry enables declaration of a Web Component via markup, and the code behind is what actually gives that component functionality.
In all current Web Components implementations, the CustomElementRegistry is accessible in the browser via the global window.customElements property. The most important method on this object is define(), which accepts three arguments, the last of which is optional. The required parameters are a custom element name and a JavaScript constructor, respectively.
The CustomElementRegistry also provides some utility functions for looking up constructors based on Web Component names and for receiving notification when specific custom element names are registered.
Shadow DOM for true component encapsulation
Although the Shadow DOM APIs aren't a requirement for creating Web Components, they constitute an extremely powerful tool for encapsulating logic and scoping customization to individual custom elements. Shadow DOM works exactly the way its name suggests, by creating a separate DOM tree in the "shadow" of the custom element to which it's attached.
Adding a shadow DOM instance to a JavaScript Web Component is very simple from a syntactical standpoint. You can set up a shadow DOM tree on any HTMLElement using the attachShadow() method. You can store a reference to the shadow DOM instance and then call the usual DOM methods on this reference. The newly created DOM tree is rendered separately from the outer DOM, and any appended elements are strictly scoped to the shadow instance. This means that even <style> blocks injected into shadow DOM instances will apply only to that instance's elements.
Thus, shadow DOM provides native functionality that you can use to create true components with locally scoped styling, DOM element selection and more. Even though it's not a vital API for creating custom elements, it's probably one of the most powerful features of the Web Components specification.
Creating reusable markup with <template> and <slot>
The HTML5 <template> and <slot> tags may not be necessarily considered a part of the Web Components specification, but HTML custom elements are a perfect use case. Since these tags are not rendered by the browser, you can declare them anywhere in your document and then use them to spin up your custom elements. The advantage of using HTML templates when creating Web Components is that you can build reusable layout using markup rather than manual DOM manipulation in JavaScript.
For example, instead of calling shadow.appendChild() for every HTML element you want to render as part of your component, you can simply declare these elements in standard HTML markup inside a <template> tag. You can then retrieve the DOM representation of the template using the content property in JavaScript. This entity can then be directly appended to the shadow DOM of the custom element, essentially loading the entire template.
One potential disadvantage of preemptively declaring custom element markup in an HTML template is that it becomes more difficult to customize content on a per-instance basis. Fortunately, the <slot> element quickly solves this issue. Within an HTML template, you can specify a <slot> element with a specific name attribute. Within the element, you should specify some default content that will appear if the slot isn't overwritten. In most cases, however, you will overwrite the slot with your own content when declaring the custom element. For example, for a slot with the name attribute equal to custom-content:
<my-element> <p slot="custom-content">This is an instance-level content specification!</p> </my-element>
When the custom element my-element is rendered, the <slot> tag in the HTML template will be replaced with a <p> tag with the specified content. This is an extremely powerful way to create reusable markup for custom elements without sacrificing customizability.
Organize Web Component projects with HTML imports
I won't go into too much detail on HTML imports because it's the most contested portion of the Web Components specification. Google first proposed HTML imports as a solution to organizing HTML templates into their own files. Many browser vendors disagree on the implementation (and need for) this aspect of the technology, however.
In essence, HTML imports would allow you to load the contents of an HTML file into another HTML file just like with CSS or JavaScript. The syntax for importing HTML content is very similar to that of CSS:
<link rel="import" href="myfile.html">
Since <template> elements are not rendered in the browser, it would make sense to separate these templates into separate files on a per-component basis and load them all when the page itself loads. That being said, because of the debate surrounding this topic, for now it's probably best to simply declare your HTML templates in the page where they will be used.
Implementing your own Web Component: "Cool Timer" example
The best way to truly understand how useful (and plain cool) Web Components can be is to see one in action! Let's walk through the typical steps taken to create a Web Component. As an example, we'll build a very simple, reusable timer.
Step 1: Define Web Component markup with an HTML template
The easiest way to encapsulate the declarative portion of a Web Component is by using the <template> DOM element we introduced above. The great thing about HTML templates is their limited scope, which also allows us to define collision-free styles. We'll also include a <slot> element so we can give each Cool Timer instance its own description. Simply add this markup to the body of your document:
<!-- Template for our timer - won't be rendered! --> <template id="cool-timer"> <style> p.timer-display { display: block; padding: 20px; border-radius: 5px; font-size: 3em; border: 1px solid #212121; color: #212121; background: #fff; } p { background: #eb6; color: #212121; padding: 5px; } </style> <p class="timer-display"> <span id="timer">0</span> seconds </p> <p> <!-- A custom description that can be overridden in markup --> <slot name="timer-description">This is the coolest timer ever!</slot> </p> </template>
Note that this step is optional, as you don't need to use an HTML template to define Web Component markup. You can create and add elements to your component's shadow DOM using JavaScript only if you'd like.
Step 2: Define an ES6 class for Web Component functionality
If your target browser supports Web Components, then it also supports ES6 JavaScript classes. This syntax is an outstanding way to add functionality to Web Components, as it allows you to encapsulate code and extend the prototype of the browser's built-in HTMLElement object.
Create a file called CoolTimer.js and add this code:
class CoolTimer extends HTMLElement {}
You'll also need to load this file from the HTML page. In the <head> section, add a normal <script> tag:
<script src="CoolTimer.js"></script>
Step 3: Connect shadow DOM and add functionality
The next step is to load the template we defined earlier and clone it for use as our Web Component's shadow DOM. You should do this in the constructor of the CoolTimer class, so go ahead and add this code inside the class block:
constructor() { // Always call parent constructor first super(); // Get template content from DOM const template = document.getElementById("cool-timer"); const templateContent = template.content; // Create new Shadow Root const shadowRoot = this.attachShadow({ mode: "open" }).appendChild( templateContent.cloneNode(true) ); }
As you can see, we load the template content using normal DOM methods. The code here that is derived from the Web Components spec is this.attachShadow(). As usual in a JavaScript class, this references the object instance of the class. In this case, that reference gives us access to all of the methods built into HTMLElement. The attachShadow() method creates a shadow DOM for each CoolTimer instance and adds the template markup as a child.
Next, we need to add the actual timer functionality to the CoolTimer Web Component. Just like components in React or Angular, Web Components have specific lifecycle hooks we can use to drive our functionality. The two we'll be using in CoolTimer are connectedCallback and disconnectedCallback. These two hooks are called when the component is connected to the DOM and removed from it, respectively. You can easily implement these (and any other Web Component lifecycle hooks) as methods in your class.
Here's the code we'll use to add them to CoolTimer:
// Called when the element is first connected to the DOM connectedCallback() { // `this` will always reference the custom element instance (which extends from HTMLElement, in this case) // First, get timer span reference const timerDisplay = this.shadowRoot.getElementById("timer"); // Set a 'second' count at 0 let elapsedSeconds = 0; // Every second, increment elapsed seconds and update timer display this.timer = setInterval(() => { elapsedSeconds++; timerDisplay.innerHTML = elapsedSeconds; }, 1000); } // Called when custom element is removed disconnectedCallback() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }
This code simply sets up a function that runs every second to increment the running timer and update the DOM element that shows how much time has elapsed. To avoid unnecessary function calls and guarantee DOM access, we only set up this interval when the component is connected, and we clear it when the component is removed.
Step 4: Register the custom element using Web Components API
Now that the CoolTimer Web Component has a template for layout and a class for functionality, we need to tell the browser how we're going to declare it. You can register Web Components via one simple call to CustomElementRegistry API interface which is surfaced as the global variable customElements in modern browsers.
At the bottom of the CoolTimer.js file, add the following line:
customElements.define("cool-timer", CoolTimer);
The first argument to this call is the name of the custom element, and it's also how we will declare it in markup. The second argument is the element "constructor" which is an ES6 class in our case.
An optional third argument can be passed to define() when creating custom built-in elements. These are components that extend native DOM elements like <div> or <p>. Our custom element is autonomous, meaning it only extends the base HTMLElement object.
Step 5: Declare the custom element in the page
All that's left to do now is actually use the Web Component we've built! It's as simple as declaring any other HTML element:
<!-- Actual, rendered instance of the cool timer --> <cool-timer></cool-timer>
If you load up the page now, you should see a timer ticking away and some default text. Now, the default text is highly engaging, but what if you wanted to create another time with a more specific description? Fortunately, overriding the default text is very easy, thanks to the <slot> we used in our template. Update the page to implement the <cool-timer> element like so (also add a separate paragraph to illustrate the scoped styles):
<!-- Actual, rendered instance of the cool timer --> <cool-timer> <span slot="timer-description">Lap 1 time</span> </cool-timer> <p>Lonely, unstyled paragraph.</p>
If you reload the page, you should see updated description text below the timer. Also note that the <p> tag outside of the Web Component instance is unstyled, illustrating the scoped nature of shadow DOM. Another interesting syntactical feature is the slot attribute. This is what matches up to the name given to the <slot> element in the component template. The tag type used (<span> in this case) can be whatever you'd like to place in the specified slot.
Browser support
An important consideration when using the Web Components spec today is browser support. The pieces of the API we used in this article are fully supported in Chrome, Opera, and Safari. Firefox has experimental support that can be enabled with flags, with full support slated for this year. The Edge team is actively working on providing their own implementation.
You can view a more thorough summary of current browser support for each technological piece of the Web Components spec here at caniuse.com.
Moving forward
Without a doubt, the Web Components specification is going to play a large role in the evolution of front-end web applications in the near future. The Accelerated Mobile Pages (AMP) framework is built using features of Web Components, and large companies are investing in tooling for the technology, such as Google's Polymer framework.
Certainly, custom elements won't replace the component engines provided by JavaScript frameworks, but there is a huge opportunity for these two paradigms to work together. It's worth noting that the JavaScript for our small tutorial weighs in at a mere 593 bytes minified and gzipped. This illustrates the power and accessibility of applications built using the native Web Components APIs and how they can complement frameworks to build more performant apps.
With more and more tools, including our own Wijmo, adding support for Web Components-based implementation, be on the lookout for more apps leveraging this new technology.
In our series of articles relating to Web Components, we offer an introduction and practical usage for Web Components and discuss how to make Web Components accessible. Learn more about shadow DOM and how to build your own Shadow Elements here.
Thanks for reading, and as always, be sure to reach out with any questions! Follow GrapeCity’s blog page for new webinars, demo videos, industry news, and more.