Why Web Components Are Ready For Production

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.

Comments

  • nice article!

  • Mike Macaulay

    Yes, this and the previous article are top notch!

  • Michael Trotter

    One extra thing to note: Google’s platform.js polyfill does not work at all on Safari 8 (releasing this fall). Webkit has mis-implemented one of the built in properties, preventing platform.js from doing its thing. We should get this bug some attention if we want to start pushing web components this year: https://bugs.webkit.org/show_bug.cgi?id=49739

    • Interesting, especially considering that ticket is so old. It’s unfortunate that their bug tracker doesn’t have the concept of stars/votes.

      • Michael Trotter

        Yeah.. more comments maybe, but I hesitate having nothing intelligent to add : ]

    • WebReflection

      out of curiosity, does my polyfill work in there ? I have no chance right now to test on Yosemite ( the answer is “yes” if these examples in this page work correctly and this page is green: http://webreflection.github.io/document-register-element/test/ ) – thanks

      • Michael Trotter

        Looks good, 42 passes, all green, no errors.

        • WebReflection

          thanks!

    • WebReflection

      out of curiosity, does my polyfill work in there ? I have no chance right now to test on Yosemite ( the answer is “yes” if these examples in this page work correctly and this page is green: http://webreflection.github.io/document-register-element/test/ ) – thanks

  • Tim

    Thank you TJ, beautifully succinct intro (even though I’m familiar with concept as I use Angular)

  • Wow! When I was playing with Polymer a little, I had this feeling that it was way too over complicated. Now having compared Polymer with document-register-element, I’m surprised how simple custom elements development can be. Great article, thank you!

  • SteveALee

    Another ‘middle way’ option is Mozilla’s x-tag [1] which uses the same platform polyfill as Polymer but does include all the ‘everything a component’ stuff. Brick adds a collection of curated components. I’ve not tried eihter in anger yet.

    1: http://x-tags.org/

    https://developer.mozilla.org/en-US/Apps/Tools_and_frameworks/Web_components

  • Don Woodlock

    Thanks for doing this – this was a very clear explanation and your endorsement of custom elements is motivating – these are really cool and useful. I like MVC but having all my view code organized by views made it a little awkward to reuse components across views – this is a really excellent approach to doing that. Andrea’s polyfill works great too. I’m going full steam on this approach now. Thanks again.

  • Great set of articles! I was inspired by this one to write an AngularJS custom element provider/service that integrates custom elements with Angular directives. It’s default polyfill is WebReflection’s.
    https://github.com/dgs700/angular-custom-element

  • Tuan Quoc

    HI Guy. What about separate css and behavior. Can i import that element in other template file ? I testing document-register-element.js

  • Mathieu Clerte

    Hi TJ, any news on that subject since august ? Any advances ?

  • Mathieu Clerte

    Hi TJ, any news on that subject since august ? Any advances ?

  • Jan-Erik Vinje

    We just tried the “document-register-elements.js” polyfill as simple replacement for the standard webcomponents.js used by x-tags and polymer. However we found that it didn’t polyfill html imports nor the template tags wich are both parts of the webcomponents standard. We are still on the lookout for a robust polyfill as we also experience trouble with the IE9 using webcomponents.js

  • Jan-Erik Vinje

    We just tried the “document-register-elements.js” polyfill as simple replacement for the standard webcomponents.js used by x-tags and polymer. However we found that it didn’t polyfill html imports nor the template tags wich are both parts of the webcomponents standard. We are still on the lookout for a robust polyfill as we also experience trouble with the IE9 using webcomponents.js

  • Pingback: Custom Elements By Example | Sysadmin Exchange()

  • Pingback: A No-Nonsense Guide to Web Components, Part 2: Practical Use | Chris Bateman()

  • Pingback: The future of frontend development is near | Yanka.pro()

  • James H. Kelly

    Great tutorial, thanks. I have what should be a simple question, but I can’t find the answer anywhere on the net. How do I get whatever css has been applied to my custom element? If I want to get the color or font-size, for example, how would I access that? I’ve tried to use getComputedStyle but it returns 0px or ‘normal’ for all attributes.

    Thanks.

  • trisys

    Great article – Are we going to see a future KendoUI with these constructs I wonder?

  • Gustavo Arias Méndez

    This article is just what I was looking for, nice one buddy

  • Gustavo Arias Méndez

    This article is just what I was looking for, nice one buddy

  • Hi TJ, nice article!
    I’ve noticed that some browsers didn’t implement shadowDom yet or it has issues. In Firefox 49.0a2 (Dev Edition) for example, I had problems with ID conflicts. I’ve created a webcomponent to handle file-inputs and used a “” that always pointed to the same (first) custom element. In Chrome, they are isolated in their shadow dom and works fine.
    I had to change my webcomponent to use events for the click.

    How do you feel about the maturity of WebComponents vs ReactJs?