Telerik blogs
Mir_header

Last month I wrote about how web components weren't ready for production yet. I stand by my conclusions in that article, but, based on feedback I received, and some subsequent research, I now believe a subset of web components — namely custom elements — are ready for most developers to use. In this article I'll explain my reasoning, and show you how to build production-ready web components.

Why custom elements?

If you're unfamiliar with custom elements, I'd recommend reading Eric Bidelman's excellent introductory article, but succinctly, the custom elements specification is what lets you type <date-picker></date-picker> instead of something like <div id="datepicker"></div><script>$("#datepicker").kendoDatePicker()</script>.

For the purposes of this article, custom elements are particularly interesting, because — unlike the other parts of web components — the custom elements spec is reasonably sane to polyfill. In total, the custom element specification defines one new method (document.registerElement()), changes to two existing methods (document.createElement() and document.createElementNS()), four callback methods (createdCallback, attachedCallback, detachedCallback, and attributeChangedCallback), and a single new CSS pseudo-selector (:unresolved).

Other than the :unresolved pseudo-class, which we'll discuss in detail later, all of these features can be implemented in JavaScript in older browsers without resorting to the shenanigans that the other web components polyfills need to. And having less methods to implement means less code — which means custom elements polyfills have smaller file sizes. In fact, the two existing custom elements polyfills are a mere 1.9K and 5.4K, respectively (after minification and gzip). Compare that to Polymer and its full suite of platform polyfills, which are 66K after minification and gzip, and you can see why custom elements are an enticing option for using web components today.

Ok, but is anyone actually using custom elements?

I'm glad you asked, as the I agree that real-world usage trumps any theoretical argument. I'll admit that I only know of one site that uses custom elements at scale, but it is a big one: GitHub. They use a custom element to implement the formatted timestamps that appear throughout the site. Here's a screenshot that shows the element in use; pay attention to the is attribute, as it declares the element as a time-ago element, which is an extension of the time element:

GitHub

Hidden deep in GitHub's obfuscated JavaScript is the code to register this extension:

document.registerElement("time-ago",{prototype:v,"extends":"time"})

Note that GitHub uses custom elements, but not the rest of the web components technologies. This element is declared in a JavaScript file and not an HTML import. It uses neither the <template> element, nor a shadow DOM in any way. GitHub's usage, and my own personal research, show this to be a sane approach for most applications today, but it's an approach that is shockingly undocumented. How do you do what GitHub did and build a web component that only uses custom elements?

Let's find out.

Building a custom element you can use in production

I don't want to provide an exhaustive guide to building custom elements, as that has already been done (and done well), but I do want to give a quick rundown of the basics. I'll use a <clock-face> custom element I recently built as a guide.

Step 1: Register the element

Start by defining a prototype for your element that is based on HTMLElement.prototype, and then register it with the browser using document.registerElement(). Here I register the <clock-face> element:

var proto = Object.create( HTMLElement.prototype );
document.registerElement( "clock-face", {
    prototype: proto
});

If you want to extend an existing element, like GitHub does with the time element, you can pass an additional extends property. For details on this approach, refer to the section of Eric Bidelman's article on extending elements.

Step 2. Pick an API

With custom elements, configuration options are regular HTML attributes. So if you're developing a new element, before diving into the code, decide which attributes your element will use and what values they will accept. For my <clock-face> element I decided on three attributes: hour, minute, and second. If the user provides no attributes — i.e. <clock-face> — the clock display will show the current time, but the user can provide attributes for the clock to display a specific time — i.e. <clock-face hour="8" minute="30" second="12">. The two approaches are shown below:

Step 3. Implement a createdCallback

The browser invokes your element's createdCallback whenever an instance of the element is created. Although the createdCallback is optional, I can't think of a situation where you wouldn't provide one, as the createdCallback is the place to initialize everything you need for your element.

For my <clock-face> element I use the createdCallback to inject some additional HTML, and set an interval that updates the clock every second. Here's the complete code:

proto.createdCallback = function() {
    var that = this;
    this.readAttributes();

    this.innerHTML =
        "<div class='clock-face-container'>" +
            "<div class='clock-face-hour'></div>" +
            "<div class='clock-face-minute'></div>" +
            "<div class='clock-face-second'></div>" +
        "</div>";

    this.updateClock();
    if ( !this.hour && !this.minute && !this.second  ) {
        setInterval(function() {
            that.updateClock();
        }, 1000 );
    }
};

// Read the attributes off the element and store references on
// the instance.
proto.readAttributes = function() {
    this.hour = this.getAttribute( "hour" );
    this.minute = this.getAttribute( "minute" );
    this.second = this.getAttribute( "second" );  
};

// This sets the CSS transform property on the three clock hands to make
// the clock work. The actual implementation is omitted for brevity.
proto.updateClock = function() {};

Step 4. Implement an attributeChangedCallback

If your custom element uses any custom attributes, you should implement a attributeChangedCallback method to update your element when those attributes change. For example, here's the attributeChangedCallback I use for my <clock-face> element:

proto.attributeChangedCallback = function( attrName, oldVal, newVal ) {
    if ( /^(hour|minute|second)$/.test( attrName ) ) {
        this.readAttributes();
        this.updateClock();
    }
};

Providing an attributeChangeCallback gives your users the ability to alter your elements through a simple setAttribute() call. For instance, suppose you want to display a clock and three number inputs.

<clock-face hour="9" minute="30" second="10"></clock-face>

<label for="hour">Hour:</label>
<input type="number" value="9" min="1" max="12" id="hour">

<label for="minute">Minute:</label>
<input type="number" value="30" min="0" max="59" id="minute">

<label for="second">Second:</label>
<input type="number" value="10" min="0" max="59" id="second">

With an attributeChangedCallback implemented, all you need to do is add a bit of JavaScript that calls setAttribute() when the value of these inputs change.

var inputs = document.querySelectorAll( "input" );
for ( var i = 0; i < inputs.length; i++ ) {
    inputs[ i ].addEventListener( "change", function() {
        document.querySelector( "clock-face" )
            .setAttribute( this.id, this.value );
    });
}

The result looks a little something like this:

In my opinion this showcases the appeal of using custom elements, as you can interact with this element using the native DOM methods you already know. Plus, custom elements are relatively simple to write. In fact, with your attributeChangedCallback method implemented, you now have a fully functional custom element. That's really all there is to it.

Ok, there is one more thing, as you still need to add a polyfill — since custom elements are only natively supported in Chrome 36+ today. Let's look at how to do that.

The custom elements spec defines two additional callbacks, attachedCallback and detachedCallback, that the browser invokes when elements are inserted and removed from the document, respectively. Most elements don't need these callbacks, so I'm not including them as explicit steps, but they're available if you need them.

Step 5. Include a Polyfill

There are two choices for polyfilling custom elements: Polymer's, and document-register-element by Andrea Giammarchi. Either can be used to polyfill the custom elements specification in browsers that don't support custom elements natively, but there are some subtle differences in how each work. Let's look at each to help you decide.

Polymer/CustomElements

Polymer publishes its custom elements polyfill in a Polymer/CustomElements GitHub repo. The polyfill is used by both Polymer (duh) and Mozilla's X-Tag library. It's also the polyfill that GitHub uses.

Getting Polymer's polyfill ready to use is an unfortunately convoluted process. For some strange reason, the custom-elements.js file that lives in the root of the Polymer/CustomElements repo is known as a “debug loader” (their term), and asynchronously loads its dependencies by injecting <script> tags. It also assumes you have the dependencies already available in a Bower-like directory structure, which is great if you're using Bower, and not so great if you're not.

To get a file that you can actually use in your app, you need to clone the CustomElements git repo, as well as its two dependent repositories — Polymer/MutationObservers and Polymer/tools — and then run its build. This is all a bit crazy, but it's the documented way of using the polyfill. The complete code needed to run the build is shown below.

$ git clone https://github.com/Polymer/CustomElements
$ git clone https://github.com/Polymer/MutationObservers
$ git clone https://github.com/Polymer/tools
$ cd CustomElements
$ npm install
$ grunt

After this completes, a custom-elements.min.js file is generated in the root of the CustomElements directory. But you're not done yet, because the build doesn't bake the polyfill's dependencies into the minified files. So you still have to manually grab MutationObserver.js and weakmap.js and include them in your project. In the end, to use Polymer's polyfill you need code that looks something like this:

<script src="/path/to/weakmap.js"></script>
<script src="/path/to/MutationObserver.js"></script>
<script src="/path/to/custom-elements.min.js"></script>

As crazy as the process is, the code itself works great. Although the polyfill officially supports only the latest version of evergreen browsers, I've found custom element support to be much better. In my testing, Polymer's custom elements polyfill works great in Chrome, Firefox, Safari, Opera, IE 9+, iOS Safari, and Android 4+. The main reason the polyfill doesn't work in IE < 9 is the difficultly of polyfilling mutation observers, which we'll discuss in a minute, but first let's talk about the other polyfill.

WebReflection/document-register-element

The second polyfill, document-register-element by Andrea Giammarchi, was specifically designed to work around the usability issues with Polymer's polyfill.

To start, using the polyfill is as simple as grabbing the document-register-element.js file from the repo — no build required. Second, document-register-element has no dependencies; everything it needs is baked in. This means that document-register-element is not only easier to use, it's also leaner, weighing in at 4K and 1.9K gzipped.

Despite the small size, document-register-element supports more browsers than Polymer. In my testing, Polymer's polyfill fails in Android < 4 browsers, but document-register-element works fine — even in the archaic Android 2.2 browser.

So which polyfill should I use?

Personally I prefer document-register-element as it's easier to use, supports more browsers, and is smaller (in terms of file size). I have been experimenting with the polyfill for several weeks now and I have yet to run in to a single issue.

That being said, the file size difference isn't huge, and older Android browsers may not matter to you. Polymer's polyfill is backed by Google and has been used in the Polymer library in numerous sites and demos — which in theory means that it has been exposed to more real-world usage.

Ultimately the decision is up to you, and there is no right or wrong answer. If you plan on developing distributable components, it's worth taking a few minutes to play with each to see which you prefer.

Step 6. Put it all together

With a polyfill in place, the only thing left to do is put the pieces together, which is just a matter of including the necessary files. A user of my <clock-face> element has to include the element's CSS, the element's JS, and a custom elements polyfill. The final code looks something like this:

<link rel="stylesheet" href="clock-face.css">

<script src="document-register-element.js"></script>
<script src="clock-face.js"></script>

<clock-face></clock-face>

That's it.

Personally I believe that this is an approach that most developers can use right now. Because custom element polyfills are small, and because they support the browsers most developers need to support, it's a reasonable dependency to add to any application that doesn't need to support IE < 9.

Wait, what about IE < 9?

Both custom element polyfills do not support IE < 9 at all. To understand why, you have to understand a bit about how these polyfills work under the hood.

Custom elements build upon a browser feature called DOM mutation observers. Succinctly, DOM mutation observers allow you to listen for DOM events, such as elements being added to the DOM and attribute changes.

Custom element polyfills use mutation observers to reimplement the callback methods that custom elements use (i.e. createdCallback, attributeChangedCallback, attachedCallback, and detachedCallback). Mutations observers are only supported in Chrome, Firefox, IE 11, and Safari 6+but, an older (and now deprecated) version of mutation observers, known as mutation events, was implemented in IE 9+, as well as the default Android browser (all the way back to 2.2). Custom element polyfills use these deprecated mutation events to add support for these older browsers.

However, because IE < 9 supports neither mutation observers nor mutation events, the polyfills have no way of implementing the required callbacks. Both polyfills have said they'd consider adding IE < 9 support if someone comes up with a sane way of polyfilling mutation events, but that has yet to happen.

However, even though custom elements are not supported in IE < 9, you can still use a graceful degradation approach to provide a reasonable experience to these users. For example, here's the raw source GitHub serves for its <time> element:

<time datetime="2014-07-17T17:25:59Z" is="time-ago">July 17, 2014</time>

IE < 9 users may not see the formatted “15 days ago” text, but they do see the unenhanced contents of the tag: “July 17, 2014”, which is a perfectly acceptable fallback. Neither polyfill throws a JavaScript error in IE 8, so you don't have to worry about conditionally including JavaScript code, you just have to consider an element's behavior if the callback functions are never invoked.

Coincidentally, this same behavior (no callback functions being invoked) is what occurs for users that have JavaScript disabled. So it's worth having reasonable fallback behavior for your elements, even if you don't care about older versions of IE.

What about :unresolved?

There's one final caveat I need to mention before wrapping this up. The custom elements spec defines a :unresolved pseudo-class that matches elements that are used, but have not yet been registered using document.registerElement(). It's a convenience selector meant to help prevent against FOUCs. For instance, I could add a clock-face:unresolved { opacity: 0; } rule to my <clock-face> element to hide elements that are used before they're registered.

However, both custom element polyfills avoid polyfilling the pseudo-class for performance reasons. I detail why in my previous article, but the core problem is that browsers reject CSS rules that they don't understand. Therefore if you want to truly polyfill a new CSS selector, you have gather all stylesheets, run regular expressions against them to find the new selector, and manually apply the CSS rules in JavaScript — which is obviously complex and expensive.

Given all of this, it's simply not worth using :unresolved until it is well supported. For now you can add a styling hook (attribute, class name, etc), style that hook in place of :unresolved, and remove that hook in the createdCallback. It's a manual process, but it's also easy to implement, and avoids the performance issues associated with polyfilling a new CSS selector.

Wrapping Up

Web components are complex and provide polyfilling challenges that make them difficult to use in browsers that don't support the technologies natively. However, custom elements have a much simpler API, which leads to simpler polyfills that can support more browsers.

As long as you don't support IE < 9, the two custom element polyfills detailed in this article provide a sane way to dip your toes into the world of web components today. It's as simple as registering your element with document.registerElement(), implementing a few callbacks, and adding a polyfill for older browsers.

Update (August 8th, 2014)

Shortly after I published this GitHub open sourced their extensions to the <time> element! Check it out at https://github.com/github/time-elements.


TJ VanToll
About the Author

TJ VanToll

TJ VanToll is a frontend developer, author, and a former principal developer advocate for Progress.

Comments

Comments are disabled in preview mode.