Web Components

Custom Elements

HTML5 is powerful enough to create modern Web applications with impressive user interface and sophisticated business logic. However, sometimes developers may wish to go beyond the predefined vocabulary of HTML elements. XML helps to solve this task to a certain degree: names of XML tags are only limited by the developer's imagination. Arbitrary XML elements can be styled by applying general CSS rules or special XSLT language. Using "XML islands" in HTML documents is an expedient method for enriching conventional markup. In addition, such "hybrid" formats as XHTML allow the developer to mix standard tags with custom elements.

XML inclusion is not the only way to build custom tags: implementation of Web Components in Chromium-based browsers has expanded the range of instruments assisting the developer to forge new elements. Creating a custom element can be as easy as declaring it in markup:

<x-custom-element></x-custom-element>

The browser supporting Web componentry will parse the custom tag declared above and register it in the context of the current document. Similar to a standard element, the x-custom-element can expose style and behavior specified in CSS and JavaScript:

CSS rule set for custom element
x-custom-element {
 display: block;
 background-image: url(surfing.png);
 background-position: center;
 background-repeat: no-repeat;
 border: 1px dotted green;
 cursor: pointer;
 padding: 9px;
 width: 238px;
 height: 136px;
}

handling click events
var customElement=document.querySelector("x-custom-element");
customElement.addEventListener(
 "click",
 function() {
  location.assign("http://en.wikipedia.org/wiki/Java_(programming_language)");
 },
 false
);

The custom element displays Duke, the Java mascot, and reacts to click events by redirecting the browser to one of Wikipedia articles dedicated to Java:

Declaring a component in HTML may prove insufficient in complex scenarios when it is preferable to create a new element in a scripting routine and enumerate its properties and methods by using JavaScript Object notation. Such approach gives the developer more control over the forged component. When all minutiae of a custom tag are specified, it is registered by calling the registerElement() method of the document:

var object=Object.create(HTMLElement.prototype);
var options={prototype: object}; // element registration options
var XCustomElement=document.registerElement("x-custom-element", options);
var customElement=new XCustomElement();
. . .
document.body.appendChild(customElement);

Registration options can indicate a standard HTML element that the custom component will extend:

var options={prototype: object, extends: "div"};

In this case resultant markup is built by the browser as

<div is="x-custom-element"></div>

A component extending a standard HTML element can be instantiated by using the special signature of the createElement() method:

var object=Object.create(HTMLElement.prototype);
var options={prototype: object, extends: "div"};
document.registerElement("x-custom-element", options);
var customElement=document.createElement("div", "x-custom-element");

JavaScript Object API

Examples above have made obvious that JavaScript Object API forms the basis for building custom elements dynamically. An object property can belong to the group of data properties or be an accessor property. A data property associates a name with a value and a set of boolean attributes. These attributes are writable, enumerable and configurable. Their practical application is grounded on the following principles:

  • if a property is writable, it can be modified without restrictions;
  • an enumerable property will be visible in for-in enumerations;
  • if a property is configurable, an attempt to delete it, change its type, or alter its boolean attributes will result in failure.

The example below uses the Object.create() method to build an object with two data properties. The static method accepts the object's prototype and the data properties descriptor as its arguments:

var object=Object.create(HTMLElement.prototype, {
 browserInfo: {
  value: navigator.userAgent
 },
 osInfo: {
  value: navigator.platform
 }
});

No boolean attributes are specified for data properties, so the default false value is applied:

object.browserInfo="custom UA string"; // the new value will be ignored
console.log(object.propertyIsEnumerable("browserInfo")); // false
delete object.browserInfo; // deletion will fail

The custom element based on the object above will use one of data properties as text content, the other - as the title:

var options={prototype: object};
var XCustomElement=document.registerElement("x-custom-element", options);
var customElement=new XCustomElement();
customElement.textContent=customElement.browserInfo;
customElement.title=customElement.osInfo;
customElement.style.color="blue";
customElement.style.fontFamily="monospace";
document.body.appendChild(customElement);

For the current browser the result could look like this:

An accessor property associates a name with one or two accessor functions and boolean attributes described above. The example below creates an object with the webResource accessor property. This time the property is enumerable:

var object=Object.create(HTMLElement.prototype, {
 webResource: {
  set: function(url) { // accessor function
   this.url=url;
  },
  get: function() { // accessor funtion
   return this.url;
  },
  enumerable: true,
  configurable: false // default value
 }
});

As the accessor property is defined as enumerable, it will be visible in the list of properties obtained from the following code:

for(var i in object) {
 console.log(i);
}

The custom element derived from the object above will emulate button behavior: a click event handler attached to the element uses the accessor property to redirect the browser to another URL.

CSS rule for custom element
x-custom-element {
display: block;
 background-image: url(objects.png);
 background-position: center;
 background-repeat: no-repeat;
 cursor: pointer;
 width: 48px;
 height: 48px;
}

creating custom element
var options={prototype: object};
var XCustomElement=document.registerElement("x-custom-element", options);
var customElement=new XCustomElement();
customElement.webResource="http://en.wikipedia.org/wiki/List_of_Ajax_frameworks";
customElement.title=customElement.webResource;
customElement.addEventListener(
 "click",
 function(event) {
  location.assign(event.target.webResource);
 },
 false
);
document.body.appendChild(customElement);

The resultant component has image-based background and directs the browser to the list of Ajax frameworks:

Both data and accessor properties can be spelled out in the static Object.defineProperty() and Object.defineProperties() methods:

var object=Object.create(HTMLElement.prototype);
Object.defineProperty(object,
 "webResource", {
  set: function(url) {
   this.url=url;
  },
  get: function() {
   return this.url;
  },
  enumerable: true,
  configurable: false
 }
);

The Object.defineProperty() method in the example above has accepted the following arguments: the object on which the property is added, the property name as a string and a descriptor for the accessor property. "Batch" addition or modification is provided by calling the Object.defineProperties(). Let's change the same example: instead of declaring the URL of the component's background image in CSS, the code below will state it as a new data property of the component's prototype.

var object=Object.create(HTMLElement.prototype);
Object.defineProperties(object, {
 image: { // data property
  value: "url(objects.png)"
 },
 webResource: { // accessor property
  set: function(url) {
   this.url=url;
  },
  get: function() {
   return this.url;
  },
  enumerable: true,
  configurable: false
 }
});

The custom element will use the data property to define the URL of its background image:

customElement.style.backgroundImage=customElement.image;

Behavioral patterns for a custom element can be designed by creating methods of the element's prototype. The following example defines a method requesting information about HTTP headers from the server where the script is residing. A readystatechange event handler is attached to an instance of XMLHttpRequest. When the XHR status code gets equal to 200, a list of headers is displayed to the user:

var object=Object.create(HTMLElement.prototype, {
 fetchHeaders: { // method
  value: function() {
   var xhr=new XMLHttpRequest();
   xhr.onreadystatechange=function(event) {
    if(event.target.readyState==4) {
     if(event.target.status==200) {
      alert(event.target.getAllResponseHeaders());
     } else {
      alert("Headers information is unavailable . . .");
     }
    }
   };
   xhr.open("HEAD", location.href);
   xhr.send();
  }
 }
});

The custom element having the object above as its prototype can launch HTTP request as a reaction to a click event:

var options={prototype: object};
var XCustomElement=document.registerElement("x-custom-element", options);
var customElement=new XCustomElement();
customElement.addEventListener(
 "click",
 function(event) {
  event.target.fetchHeaders();
 },
 false
);
document.body.appendChild(customElement);

Callback Functions

When a custom element is instantiated, added to markup of the document, or removed from the current context, a range of events is dispatched to registered listeners. These are created, attached and detached. Let's return to the first example of the article and redefine the component with surfing Duke: this time style and behavior of the element will be specified within event handlers.

var object=Object.create(HTMLElement.prototype);
object.createdCallback=function() {
 console.info("Custom element is created.");
 this.style.display="block";
 . . .
 this.addEventListener("click", function(event) {
  document.body.removeChild(event.target);
 },false);
};
object.attachedCallback=function() {
 console.info("Custom element is attached to the document.");
};
object.detachedCallback=function() {
 console.info("Custom element is detached from the document.");
};

var options={prototype: object};
var XCustomElement=document.registerElement("x-custom-element", options);
var customElement=new XCustomElement();
document.body.appendChild(customElement);