July 12, 2015

Using HTML5 right now: Web components by example (part 2 of 2)

Native HTML5 JavaScript

When targeting browsers with native HTML5 custom components support only, there is no need to import 3rd party JavaScript polyfill shims. You can just rely on the browser’s native JavaScript API to implement custom elements.

Check availability

First of all, you want to check whether the user’s browser supports native HTML5 custom components. This JavaScript code does that:
if (!document.registerElement) {
    alert('Custom elements not supported!');
}
else {
    // build custom elements here
}
When running in a vanilla Firefox 39 installation, the alert will pop up. By enabling experimental HTML5 custom elements support with about:config setting
dom.webcomponents.enabled = true
or when using Chrome 36+, the alert won’t show up indicating native support is provided.

Implementation

Now we are ready to implement our custom element! This is all the JavaScript code we need:
var MyInfoboxProto = Object.create(HTMLElement.prototype);

MyInfoboxProto.createdCallback = function() {
    var stars = this.getAttribute('stars') != null ? this.getAttribute('stars') : 0;
    var name = this.getAttribute('name') != null ? this.getAttribute('name') : 'Rating';
    this.innerHTML = "<span class='my-infobox my-rating" + stars + "'>" + 
        name + "<i class='fa fa-lg'></i></span>";
};

document.registerElement('my-infobox', {
    prototype: MyInfoboxProto
});
We can then use the custom element in HTML code, as in
<my-infobox stars="2"></my-infobox>
which renders
Rating
There are a few important things happening in this JavaScript code which enable the magic:
  • var MyElementProto = Object.create(HTMLElement.prototype); lets you derive your custom element from the base HTML element’s prototype. You can actually use any existing HTML element (tag) as a base for your custom element.
  • Building the component usually happens in the createdCallback function. There are three more callback functions available: attachedCallback, detachedCallback, attributeChangedCallback(attrName, oldVal, newVal).
  • You can manipulate any property of your element’s prototype in the callback. The most concise way to update the rendered HTML code certainly is to set this.innerHTML.
  • With this.getAttribute('name'), you can retrieve a tag instance’s attribute value. In the example, we check whether they are set and use a sensible default value otherwise.
  • The actual registration happens in document.registerElement(…). Note that a custom tag name must include a dash character (-), e.g. my-element.
That’s all we need for this example! You may want to take a look at the complete example code on GitHub.

For more information about how to write custom elements, check out these useful tutorials as well:

webcomponents.js

Installation

If you happen to work in a Node.js infrastructure, you can use the bower dependency manager to install the required JS file. Otherwise, the simplest way to download the file I found was to download the ZIPed distribution from the Polymer project which contains a minified webcomponents-lite.min.js file. This is the only file we need for pure webcomponents.js.

Implementation

As I briefly explained earlier, webcomponents.js is a fully standard-compliant shim for the HTML5 web component JavaScript API. This means that you can write the same JavaScript code and it will work either with native browser support present, or with the JavaScript shim fallback.

Try it out! Link the webcomponents.js script file in the “native” implementation:
<script src="webcomponents-lite.min.js"></script> 
In Firefox, you can then disable experimental support for HTML5 custom components with
dom.webcomponents.enabled = true
the “not supported” alert will never pop up, and the custom element will perfectly work.

Of course, you will then remove the support test as it is performed by the shim already.

Click here to see the complete source code on GitHub.

Polymer

Installation

Again, use the bower dependency manager to install the required JS files. Or download the ZIP from the Polymer homepage. For custom element, we just need to link to the polymer.min.js and webcomponents.min.js files in our HTML page.

Implementation

With Polymer, you can define custom elements either in JavaScript, similar to how it works in plain HTML5 code, or you can do it the declarative way using specialized HTML tags. For illustration purposes, I will here use this second approach.

The complete solution reads as follows. This Polymer element goes in the HTML page’s <header>:
<polymer-element name="my-infobox" attributes="name stars">
    <template>
        <link rel="stylesheet" href="mystyle.css">
        <link rel="stylesheet" 
            href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
        <span class="my-infobox my-rating{{stars}}">{{name}}<i class="fa fa-lg"></i></span>
    </template>
    <script>
        Polymer({
            name: "Rating",
            stars: 0
        });
    </script>
</polymer-element>
We can then use the custom element in HTML code, as in
<my-infobox stars="2"></my-infobox>
which renders
Rating
What is happening in the Polymer code?
  • The <polymer-element> umbrella tag specifies the element with the name provided and “publishes” an arbitrary number of attributes for use with the tag.
  • The <template> sub-tag defines the rendered content. Note that we have to link to all the required external files, e.g. stylesheets. Having them in the containing HTML file itself will not suffice! What’s strange is that apparently, the link to mystyle.css could then be completely removed from the containing HTML file whereas the link to font-awesome.min.css must be present in both the containing HTML file and the polymer-element’s template section in order to render correctly. I don’t understand that yet.
  • Most importantly, in the <template> sub-tag, we can refer to the element’s attributes by putting them in double angle brackets (handlesbars style).
  • The <script> sub-tag allows us to define default values for the element’s attributes.
If you go for this declarative approach, you’ll have to include your custom elements in other HTML files by using HTML imports, rather than <script> links:
<link rel="import" href="elements/my-element.html">
Note that this does not work when loading from file://, thus it’s not an option for static HTML resources.

Of course, Polymer works either with native browser support or by falling back on the webcomponents.min.js shim. In the latter case, however, performance impact is clearly noticeable. Because the declarative <polymer-element> solution works only with the fourth part of the web components specification, Shadom DOM, enabled, we must use the full-blown webcomponents.min.js rather than the “lite” version. Also note that you must link to that JS file first, before linking to the polymer.min.js.

Click here to see the complete source code on GitHub. 

X-Tag

Installation

The required x-tag-core.min.js file is available in the X-Tag core GitHub repository.

Implementation

X-Tag supports imperative JavaScript-based custom element creation only. The source code will thus typically look like a more simplified version of vanilla HTML5 custom element code.

This is the complete solution:
xtag.register('my-infobox', {
    lifecycle: {
        created: function() {
            var name = this.getAttribute('name') != null ? this.getAttribute('name') : 'Rating';
            var stars = this.getAttribute('stars') != null ? this.getAttribute('stars') : 0;
            this.innerHTML = "<span class='my-infobox my-rating" + stars + "'>" + 
                name + "<i class='fa fa-lg'></i></span>";
        }
    }
});
The interesting part here is that
  • xtag.register registers the element in question.
  • Although the second argument to the register function (here: {lifecyle: …}) accepts a content element to define the rendered markup as a plain String, I don’t think this works with integrating the element’s attribute values. There is just no templating mechanism as provided by Polymer. Thus I included the attribute values the imperative way.
Again, X-Tag either uses built-in native web components support or uses its internal JavaScript fallbacks. Performance impact seems hardly noticeable.

Click here to see the complete source code on GitHub.

Bosonic

The Bosonic Polyfill offers declarative API similar to the one offered by Polymer.

Because it doesn’t seem to offer more features than the other polyfill libraries, and because it doesn’t stand out particularly when looking at its file size or its project health, I decided to not give it a closer look here.

document-register-element

Installation

The required document-register-element.js file (minified) is available in the GitHub repository.

Implementation

document-register-element.js uses an imperative syntax very similar to the one used by X-Tag to register custom elements:
document.registerElement(
    'my-infobox', {
    prototype: Object.create(
        HTMLElement.prototype, {
            createdCallback: {value: function() {
                var name = this.getAttribute('name') != null ? this.getAttribute('name') : 'Rating';
                var stars = this.getAttribute('stars') != null ? this.getAttribute('stars') : 0;
                this.innerHTML = "<span class='my-infobox my-rating" + stars + "'>" + 
                    name + "<i class='fa fa-lg'></i></span>";
            }
        }
    })
});
At this point, I don’t think I have to explain this code any further.

The true miracle about this polyfill is that with its tiny 7 KB footprint it supports even more browsers than the original webcomponents.js. Note however that this polyfill really only contains the custom elements part of the web components specification!

For pure custom elements however, it works great with both native support and its own fallback without noticeable performance impact.

Click here to see the complete source code on GitHub. 

There’s more

You may also want to take a look at the other three parts of the web components specification. Some custom elements polyfills, most prominently Polymer, have additional support to facilitate usage of that functionality as well. Especially the Shadow DOM specification nicely interplays with custom element as it allows you to encapsulate or opt-out DOM elements on the fly. I will potentially publish another post about these topics at some later point.

Also, the pure existence of these custom element generators makes it possible to actually create business-case agnostic reusable components. Thus alongside these polyfill libraries, custom element repositories have emerged, from which you can take any custom element of your choice and include it in your HTML pages. This really enables a whole new level of native HTML reusability. Some of these catalogues are featured on the webcomponents.org homepage in the “Discover” section – go have a look!

Conclusion

I was surprised to find a very diversified exosystem all up and running, implementing what is commonly still considered the “web technology of the future”. Unfortunately, this diversified ecosystem looks somewhat confusing at first sight. But after taking a closer look, I am now convinced that this not only has great potential for the near future, but is also very useful right now! Apart from simple, static HTML pages (such as this blog), I think one should seriously consider including a web component abstraction layer in a HTML project, and most definitely when targeting modern browsers exclusively. This is especially true for HTML-heavy client side web application which do not already include component-based 3rd party frameworks (as e.g. AngularJS).

The true question, in my opinion, is thus not whether to include a web component solution, but which library to use for it. From what I have tried out so far, I came to this conclusion:
  • If for whatever reason you are lucky enough to target Chrome only, I would consider native web component development with webcomponents.js as a fallback just in case.
  • If you’re planning to use web components excessively, you should consider going with the Polymer library. Its declarative approach and the handlesbars-like inline expressions will certainly increase maintainability even with a large code base. Due to noticeable performance impacts, this should target browsers with native web components support only. This solution is clearly targeted for heavy client-side solutions.
  • If, on the other hand, you just want to use web components sparingly or if you have strong responsiveness constraints, you should really consider the document-register-element library. The only disadvantage of this solution is that you lose standard API compatibility.
Please let me know in the comments section if this article was helpful for you or whether it lacks some important information. Given that I don’t receive too many discouraging comments, I will consider turning this into a whole HTML5-themed article series…


Pages: 1 2

No comments:

Post a Comment