Web Components
Polyfills
Web Components are sometimes dubbed "the Web technology of the future". In some degree this assertion is not an exaggeration. When software componentry gets implemented in all major browsers, application development will become more streamlined: custom elements with encapsulated internal APIs will be used as interoperable pieces of an application's "edifice". By now current implementation of Web Components and HTML <template> element has the following status:
HTML Imports Custom Elements Shadow DOM <template>
HTML Imports Custom Elements Shadow DOM <template>
HTML Imports Custom Elements Shadow DOM <template>
<template>
While components are not universally supported, polyfills are used to bring Web componentry to any browser. Polyfills are loaded as standalone JavaScript libraries, or employed as part of a full-featured framework such as Polymer. Polymer's fundamental principle is based on the Everything is an element idea:
You take a few low-level elements, put them together, and make a larger, more powerful element with its internals safely encapsulated. You can take those elements and build even bigger and better elements. Before you know it, you'll arrive at an entirely encapsulated, reusable app.
Polymer is built on top of Web componentry but it cannot be reduced to components only. One of Polymer's important features is two-way data binding - a programming technique providing separation between the user interface and its underlying data (model): a model update will be immediately propagated to the UI; the UI modification will instantly change the model. Polymer's suite of elements consists of two groups. The first group embraces the Core Elements providing the basic functionality of the Web application. The second group is the Paper elements implementing Google's Material Design - the most promising Web innovation of 2014.
If an application does not require full-scale componentry, it can load a standalone polyfill. Let's consider practical examples of using polyfill libraries in the environment lacking native implementation of Web Components.
HTML Templates
When Web Components were just starting their way to the realm of real-world applications, they were based on four client-side constituents - templates, decorators, custom elements, and shadow DOM. Since then approach to the proposed component model for the Web has changed. Decorators were found redundant and, as a result, were discarded, and templates became integral part of HTML5 specification: the <template> element is included in the set of standard HTML elements. At present HTMLTemplateElement interface is available in all major browsers except Internet Explorer.
In spite of the fact that templates have been integrated into HTML5 vocabulary, they are indispensable part of any mature application using Web Components: templates contain inert DOM subtrees that can be activated to build shadow DOM nodes or to create a custom element. Polymer uses the <template> to define HTML content that is cloned into the shadow DOM of each instance of a Web component.
To test implementation of templates in the browser, standard JavaScript tweaks can be applied: for example, if templates are unavailable, the __proto__ property of a template will hold a reference to the prototype of HTMLUnknownElement.
var template=document.createElement("template");
var isUnknown=eval(template.__proto__===HTMLUnknownElement.prototype);
if(isUnknown==true) {
console.info("Your browser does not recognize templates. Use polyfills . . .");
}
Another handy tool to check the availability of HTML elements is the typeof operator: it returns a string identifying the data type of a specified object. If templates are not implemented in the browser, the type of HTMLTemplateElement is represented as undefined:
var template=typeof HTMLTemplateElement;
if(template=="undefined") {
console.info("Your browser does not recognize templates. Use polyfills . . .");
}
To launch an application depending on templates in browsers not supporting them natively, the Template.js
polyfill from the Polymer project should be used. The example below loads the polyfill to create a template-based Christmas card:
style rules for template nodes
<link rel="stylesheet" type="text/css" href="style-rules.css">
HTML Templates polyfill
<script src="Template.js"></script>
. . .
template with image and text
<template>
<img src="Christmas-Card.png" width="64px" height="64px"> Merry Christmas!
</template>
. . .
<div></div> container for template nodes
template activation in script
window.addEventListener("load", function() {
var template=document.querySelector("template");
document.querySelector("body > div").appendChild(template.content.cloneNode(true));
});
HTML Imports Polyfills
Web Components API is implemented in Chromium-based browsers in its standard notation. As for Firefox, Web componentry is disabled in Mozilla's browser by default: to start using it, the developer should toggle the dom.webcomponents.enabled preference value. Furthermore, HTML Imports may be removed from future releases of Firefox. An article explaining Mozilla's attitude to Web Components has been published on hacks.mozilla.org this month:
Mozilla will not ship an implementation of HTML Imports. We expect that once JavaScript modules - a feature derived from JavaScript libraries written by the developer community - is shipped, the way we look at this problem will have changed. We have also learned from Gaia and others, that lack of HTML Imports is not a problem as the functionality can easily be provided for with a polyfill if desired.
We can agree with Mozilla that it is rather easy to create a polyfill importing an external HTML file:
Christmas-tree.html
<div>
<img src="Christmas-tree.gif" width="60px" height="72px">
</div>
main-file.html
<link rel="import" href="Christmas-tree.html">
minimalistic polyfill
var link=document.querySelector("link[rel='import']");
if((typeof link.import=="undefined")==true) {
var xhr=new XMLHttpRequest();
xhr.onload=function(event) {
if(event.target.status==200) {
var parser=new DOMParser();
var importedDocument=parser.parseFromString(event.target.responseText, "text/html");
var importedContent=importedDocument.querySelector("body > div");
document.body.appendChild(importedContent);
}
};
xhr.open("GET", link.href);
xhr.responseType="text";
xhr.send();
}
If the browser supports the document response type (xhr.responseType="document";
) and returns the response as an instance of HTMLDocument, then the polyfill above could be more succinct:
var importedDocument=event.target.response;
. . .
The developer can avail himself of the HTML Imports polyfill from webcomponents.org: the polyfill is brought into play as a standalone HTMLImports.js
file, a comprehensive webcomponents.js
library embracing Imports, Custom Elements and Shadow DOM, or a lightweight webcomponents-lite.js
which does not support Shadow DOM API. In any case, imports processing should be launched within the scope of the HTMLImportsLoaded event handler:
polyfill
<script src="webcomponents.js"></script>
file to import
<link rel="import" href="Christmas-tree.html">
adding imported elements to the current document
window.addEventListener("HTMLImportsLoaded", function(event) {
var link=document.querySelector("link[rel='import']");
document.body.innerHTML=link.import.body.innerHTML;
});
Even if a high-quality polyfiil is available for a certain Web technology, it cannot compete with its native implementation. We suppose that component-related capabilities should be entirely implemented in any browser, and HTML Imports should not be discarded.
Custom Elements Polyfills
Both Chrome and Opera support Custom Elements functionality. Mozilla will enable custom elements in Firefox in the near future:
Mozilla will ship an implementation of custom elements. Exposing the lifecycle is a very important aspect for the creation of components. We will work with the standards community to use Symbol-named properties for the callbacks to prevent name collisions. We will also ensure the strategy surrounding subclassing is sound with the latest work on that front in JavaScript and that the callbacks are sufficiently capable to describe the lifecycle of elements or can at least be changed in that direction.
<> Mozilla's experiments with custom elements had started long before the technology struck its root in modern browsers: Mozilla's X-Tag library is a small, but efficient polyfill helping the developer to create components and monitor their lifecycle through event handlers. Auxiliary methods of X-Tag provide object-to-array conversion, dynamic toggling of CSS classes and events delegation.
The example below creates a custom element converting numbers into their binary, octal and hexadecimal equivalents. The component has four child nodes: an <input> with a special prototype and three <div> elements displaying dynamically updated values. Conversion is performed even if the browser does not support the <input> with the type attribute equal to number. For demonstration purposes, style rules for the nodes of the custom element are specified in the head of the document:
loading X-Tag polyfill
<script src="x-tag-components.js"></script>
style rules for child nodes of the custom element
<style type="text/css">
input {
border: 1px dotted silver;
color: cornflowerblue;
}
.conversion-result {
color: darkcyan;
font-family: sans-serif;
}
</style>
The prototype property of an object allows the developer to add new methods and properties to standard elements. In this example four API members are appended to the prototype of HTMLInputElement:
HTMLInputElement.prototype.binary=new String(); // new property
HTMLInputElement.prototype.octal=new String(); // new property
HTMLInputElement.prototype.hexadecimal=new String(); // new property
HTMLInputElement.prototype.convert=function() { // new method
if(this.value.length>0) { // a certain value is inserted by the user
var number=new Number(this.value); // can the value be used as a number?
if(isNaN(number)==false) { // the value is a number
this.binary=number.toString("2"); // binary radix
this.octal=number.toString("8"); // octal radix
this.hexadecimal=number.toString("16"); // hexadecimal radix
return true; // conversion is performed
}
}
return false;
};
Registration of the custom element in X-Tag is provided by calling the static xtag.register() method. The method accepts the name of the element and an object representing registration options. The developer can monitor the lifecycle of generated elements by specifying callbacks for created, inserted, removed and attributeChanged events. Other options may indicate a prototype of the custom element, enumerate its properties and methods, or define the element's reaction to events.
The example creates the converter as an instance of HTMLElement and builds its subtree within the scope of the created callback. Such auxiliary methods as xtag.addEvent() and xtag.addClass() characterize additional features of the custom element:
custom element registration
xtag.register("x-converter", {
prototype: Object.create(HTMLDivElement.prototype), // custom element prototype
lifecycle: {
created: function() { // element creation callback
var input=document.createElement("input"); // first child node of the element
xtag.addEvent(input, "input", function(event) {
if(event.target.convert()==true) {
event.target.parentNode.childNodes[1].textContent=event.target.binary;
event.target.parentNode.childNodes[2].textContent=event.target.octal;
event.target.parentNode.childNodes[3].textContent=event.target.hexadecimal.toUpperCase();
} else {
event.target.parentNode.childNodes[1].textContent="";
event.target.parentNode.childNodes[2].textContent="";
event.target.parentNode.childNodes[3].textContent="";
}
}
);
adding child nodes to the custom element
this.appendChild(input);
var binaryValue=document.createElement("div");
xtag.addClass(binaryValue, "conversion-result");
this.appendChild(binaryValue);
. . . adding two more <div> elements for displaying octal and hexadecimal values . . .
}
}
});
The registered element can be instantiated dynamically and appended to the document's body:
var customElement=document.createElement("x-converter");
document.body.appendChild(customElement);
The resultant UI might look like this:
The same element can be built by using the Custom Elements polyfill from webcomponents.org. Scripting techniques resting upon the polyfill are similar to the code based on the native implementation of Custom Elements API:
polyfill
<script src="webcomponents.js"></script>
building the custom element in script
var object=Object.create(HTMLElement.prototype);
object.createdCallback=function() {
. . . creating subtree of the custom element . . .
};
custom element registration
document.registerElement("x-converter", {prototype: object});
Shadow DOM Polyfill
Shadow DOM is supported by Chrome and Opera and will be enabled by default in future releases of Firefox:
Mozilla will ship an implementation of shadow DOM. We think work needs to be done to decouple style isolation from event retargeting to make event delegation possible in frameworks and we would like to ensure distribution is sufficiently extensible beyond Selectors.
If the browser does not support shadow subtrees natively, the only way to use them is to launch the Shadow DOM polyfill.
polyfill
<script src="webcomponents.js"></script>
building composed DOM
var shadowHost=document.createElement("div");
shadowHost.innerHTML="<img src='Happy-New-Year.png' style='border: 4px groove #F8AA47;'>";
var shadowRoot=shadowHost.createShadowRoot();
shadowRoot.innerHTML="<div><content></content></div>";
document.body.appendChild(shadowHost);
Summary
Polyfills use the existing browser infrastructure to provide the encapsulation and extendability of both standard and custom elements. If the browser has already adopted software componentry, polyfills switch to the native implementation. Let's hope that Web Components will become available in all flagship browsers next year: the more powerful interfaces developers have at their disposal, the more interesting and innovative applications they can create!