Why Web Components — Does the Web Really Need Another Component?

What do Web Components fundamentally solve?

Shaun Wallace
13 min readNov 12, 2014

Building complex user interfaces has typically consisted of bringing together multiple web-based technologies that intertwine HTML templates, css styling rules and javascript logic. These can span small snippets of code like adding a simple hover action to large complex architectures that handle numerous controller and view based logic. In previous years, prior to the introduction of front-end frameworks, which aim to alleviate some of the compiling of various assets into a decoupled system, static HTML templates coupled tightly with javascript plugins were pieced together and placed into production environments without any sort of reliability. Some library specific script or css style being used in one environment may collide with those from another. Following common patterns like the module pattern helped to mitigate some javascript collisions via namespacing and encapsulation but the process of joining multiple js libraries together with templates and styling still quickly becomes hard to manage, debug and quite disjointed in nature.

Web Components allow us to unite markup and styles into custom HTML elements. These “blackboxes” form new markup, which affords the opportunity for a more semantic structure that encapsulates all of their HTML and CSS. This ultimately means that what you write is what you get. You do not need to be concerned with the presentation and/or access control from other scripts and styles. 1

“Web Components are a natural evolution of HTML. HTML is too basic to allow us to create App interfaces. When we defined HTML5 we missed the opportunity to create semantic widgets […]. We limited the scope of new elements to what people already hacked together using JS and the DOM. Instead we should have aimed for parity with richer environments or desktop apps. But hey, hindsight is easy.” – Chris Heilmann

They allow for a standard way of defining and using ui widgets and plugins. They can be used with any js library and they do not even need to include ui components to work, for example polymer-ajax. They may only include a standard way of performing a common task like making ajax requests.

“Web Components usher in a new era of web development based on encapsulated and interoperable custom elements that extend HTML itself”. 2

How are they constructed?

Templates

Templates allow you to create reusable/cloneable pieces of HTML that can be used throughout your application. They are similar to what other templating engines provide, those like Mustache and Underscore. Everything that should be part of this template will live between <template></template> tags. All markup and external asset references within this tag will not be excuted by the browser. So for example, image sources will not cause http requests to be triggered nor do scripts within <script></script> tags execute. This essentially means that templates that do include external resources like images can leave their src=“” left blank which in turn will not cause any 404 errors and can later be dynamically updated. In addition, nothing from within the template itself will be rendered to the page without being activated via javascript.

Template Essentials…

  1. Its content is effectively inert until activated. Essentially, your markup is hidden DOM and does not render.
  2. Any content within a template won’t have side effects. Scripts don’t run, images don’t load, audio doesn’t play,…until the template is used.
  3. Content is considered not to be in the document. Using document.getElementById() or querySelector() in the main page won’t return child nodes of a template.

Templates can be placed anywhere inside of <head>, <body>, or <frameset> and can contain any type of content which is allowed in those elements. Note that “anywhere” means that <template> can safely be used in places that the HTML parser disallows…all but content model children. It can also be placed as a child of <table> or <select>. 2

In summary, templates aim to eliminate the problems with client side templating that use the following approaches:

  1. Including html that has an initial hidden state using css styles, which has the drawback of fetching external resources even if the content is never visible to the user.
  2. Storing the template inside of a <script> tag which could possibly be vulnerable to cross site scripting if not properly handled.

Activating A Template…

Like previously mentioned, anything that lives within a template will not be useable and/or rendered until activated via javascript. Eric Bidelman notes that the easiest way to make a template’s source active is by, “creating a deep copy of its content”. Which can be done via document.importNode(). Note: document.importNode() is widely supported on modern browsers but mobile support is unknown except for FF Mobile(Gecko) based on this browser compatibility chart. Once a template has been appended its content essentially is now live and will function normally and will be rendered.

https://gist.github.com/shaunwallace/10d9364c3cd3fae5863e

Live Example: http://jsfiddle.net/shaunwallace/057r3fpa/

For more info on the specification see:

  1. https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
  2. http://www.html5rocks.com/en/tutorials/webcomponents/template/
  3. http://webcomponents.org/articles/introduction-to-template-element

Shadow DOM

The shadow DOM essentially solves the encapsulation problems that occur when you have multiple scripts and styles running within the same environment. It allows the browser to include a subtree of DOM elements into the document but outside of the main document DOM tree. “It provides a way of establishing and maintaining functional boundaries between how DOM trees interact with each other within the document.”3 A common example for understanding the Shadow DOM is the HTML5 <video> element. Upon further inspection, with the use of Chrome’s dev tools flag found in settings > general > Show Shadow DOM, we can see a better representation of what is going on behind the scenes. Everything inside the <video> tag has been wrapped inside a document fragment. Essentially what this does is create a lightweight document object that has no parent. This object is a minimal version of the document object and is able to store well-formed and potentially non-well-formed fragments on XML. DOM injections and modifications are generally slow operations so the goal is to keep this number low and this is where the real power of document fragments comes into play. This DocumentFragment is like a pseudo DOM node and has its own api which can be used similarly to a normal DOM node but without expensive DOM operations. Once this tree is ready it can simply be appended to its parent node. 4

Using DocumentFragments is faster than repeated single DOM node injection and allows developers to perform DOM node operations (like adding events) on new elements instead of mass-injection via innerHTML. — David Walsh

Every element in HTML is considered to be a node. Nesting nodes within other nodes forms a node tree.

Shadow DOM is unique in that it allows us to create our own tree structures known as shadow trees. These trees fully encapsulate their contents and only render what is specified.

https://gist.github.com/shaunwallace/df3995f4ab181a4e5160

From the output of the above code, what is important to note is that the text content that is part of the <section> tag is not rendered. Only the content that we placed inside of the shadow root, not the shadow host.

Now in order to essentially use the content from our template we need to use the new <content> tag. This allows us to take the content from the shadow host and combine it with the structure of our template which becomes our shadow root. This is useful in that it allows us to separate out the content and presentation layers. The shadow host holds the content while the shadow root contains the presentation.

https://gist.github.com/shaunwallace/c38399d9807d4ea85431

The output from this example would effectively contain <h1>Hello, John Smith!</h1>. The <content> tag creates what is called an insertion point. These places of insertion allow, “for the changing of the order in which things render without physically altering the source.” As well as allowing for us to decide what gets rendered. 5

From the example above you may also note that the shadow DOM is not being entirely constructed via javascript but rather uses the <template> as the basis for the the content of the shadow root.

It is possible for the shadow DOM to be constructed with multiple insertion points using what is called selects. These selects are simply attributes placed on elements in which we want to select what to display. For example, <content select=“.first-name”>. This will find any elements with the class of name inside of the shadow host and render its content inside of the shadow root. It is also possible to change the order of the how this content is presented within the shadow root. All that is needed to make this happen is to reorder the template which in-turn does not affect the content of the shadow host. Lastly, there is a greedy select which is a wildcard selection and the following are all equivalent:

  1. <content></content>
  2. <content select=“”></content>
  3. <content select=“*”></content>

In using insertion points we have allowed the content of the shadow host to be projected into the spot we want in the shadow root. If at anytime in the future we want to update the shadow host content we can via document.querySelector(‘.info-car’).textContent = ‘Jane Doe’; and this would automatically update the projected value in the presentation layer which is the shadow root.

The real benefit of separating the presentation from the content is that at anytime in the future we can easily change the presentation of our template without affecting the semantics of the content.

Style Encapsulation

One the important properties of the shadow DOM is what is known as the shadow boundary which provides style encapsulation. CSS styles defined inside of the shadow DOM are scoped to the shadow root which happens by default. All other styles defined externally or on the page can not access the nodes within the shadow root. We can style the shadow host element using the :host selector but one thing to note is that rules from the parent document have a higher specificity than :host but lower specificity than a style attribute defined on the host element. Therefore the host styles can be overridden. Also, the :host selector can only be used in the context of the shadow root and not outside of the shadow DOM. More information on styling and scope can he found here.

Multiple Shadow Roots

It is possible for a shadow host to have more than one shadow root at a time. The addition of new shadow roots to a host works similarly to that of a LIFO stack in that the last root to be added will override the previous or “older” tree. The last or “youngest” tree added is the one that is rendered.

Shadow Insertion Points

Shadow insertion points function similarly to normal insertion points, previously explained, in that they are placeholders where content is projected. The difference is that instead of being placeholders for content they are placeholders for other shadow trees. To overcome the LIFO stack implementation that occurs when multiple shadow trees are created we need to use shadow insertion points so that the older trees are also rendered. See this live example here. There is an api for the shadow host that gives you access to the the oldest shadow root as well as getting the host’s shadow root which is its youngest. Eric Bildelman has created a tool for visualizing how the shadow DOM is rendered and can be useful to help better understand insertion points.

The Event Model

Since encapsulation is a key part of the shadow DOM events that are triggered can be stopped at the shadow boundary. Events that do cross the boundary are retargeted so that they do not pass through the shadow’s upper boundary. Events that have been triggered on child nodes within the shadow host are adjusted to look like they have come from the shadow host itself. Also, events triggered on elements that are internal to the shadow DOM do not bubble through the upper boundary but elemements/distributed nodes in the host will trigger events. So for example an <input> element with a :focus event attached to it will not be the target of the event when fired if the input is node within the shadow DOM but the event target would be reassigned to that of the shadow host. For a list of events that never cross the shadow boundary see here.

Custom Elements

Custom elements allow web developers to define new types of HTML elements. Authors can associate javascript code with these elements and then these elements can be used just like any other HTML tag. They allow for the following:

  1. Define new HTML/DOM elements
  2. Create elements that extend from other elements
  3. Logically bundle together custom functionality into a single tag
  4. Extend the API of existing DOM elements

As with any other element they can be created via javascript or declared. The thing to note is that the naming convention for custom elements must always contain a dash. This restriction allows the parser to determine which elements are native and which are custom and allows for forward compatibility for the introduction of future tags that are added to HTML. Common practice is to namespace your element as the first part of the element’s name for example, <namespace-elementname>.

Before you can actually use a custom element you must first register it via:

var element = document.registerElement('x-element'); document.body.appendChild(new element());

There are 2 parameters that can be used with declaring a new element. The first is the element’s name described above and the second, which is optional, is the object that describes the element’s protoype. By default custom elements inherit from HTMLElement. If you wanted to extend a native HTML element then you are required to pass the name and the prototype that the element is extending to the resigterElement() method.

var superButton = document.registerElement('super-button', { prototype : Object.createElement(HTMLButtonElement.prototype), extends : 'button });

It is also possible to extend another custom element in the same fashion as extending a native element. All elements with valid names will by default extend from HTMLElement but those with invalid names will extend from HTMLUnknownElement. For all of the ways in which you can instantiate custom tags see here. One thing to note is that when using a custom element that extends from something other than HTMLElement when it was registered, then use the “is” syntax <button is=“x-foo-button”></button>.

Properties and Methods

When defining a custom element it is possible to combine its definition with its properties and methods. There a couple ways of doing this:

https://gist.github.com/shaunwallace/aaa0944f2d74e547d2f1

Lifecycle callbacks

Every custom element can make use of callback methods that allow for appropriate functionality to be run at the right time. A list of these possible callbacks can be found here. All of the callbacks are optional and can be defined as follows:

var proto = Object.create(HTMLElement.prototype); proto.createdCallback = function(){...}; var xFoo = document.registerElement('x-foo', {prototype: proto});

A common use case for using callbacks would be setting up and tearing down on element instantiation so for example binding/unbinding event listeners. Putting it all together to use custom elements with templates and the shadow DOM:

https://gist.github.com/shaunwallace/ac115fbbffeb5bb509b7

live example: http://jsfiddle.net/shaunwallace/e9f1qmeg/

Shadow DOM infuses an element with style encapsulation. Styles defined in a Shadow Root don’t leak out of the host and don’t bleed in from the page. In the case of a custom element, the element itself is the host. The properties of style encapsulation also allow custom elements to define default styles for themselves. — Eric Bidelman

HTML Imports

All resources needed on the web have pretty straight forward methods for requesting those resources. HTML is an exception to this rule. Javascript, CSS, images, video all have declarative ways to say, “hey get me this stuff now”, but if you wanted to include snippets of HTML the current way to go about doing this is either through iframes, ajax, or embedded strings. With HTML imports, we now have a standard method for including HTML documents within other HTML documents. These imports are not limited to HTML only but can include CSS, JavaScript, or anything else that can live within an HTML file. They provide a convention for packaging up all needed assets into a single include statement.

<link rel="import" href="/path/to/imported/assets.html">

A couple things to note include:

  1. Assets that have previously been requested are only retrieved once. No matter how many times an import located at the same path is loaded, it is only execute at most once.
  2. To retrieve assets from another domain CORs must be enabled at that domain.

So what exactly happens when the parser hits the import line in your code? It doesn’t just grab the resource and place it directly into your document. In order to get ahold of the content of the imported document you must interact with it programatically.

var content = document.querySelector('link[rel="import"]').import;

It is also possible to clone parts of the import into the main document via:

var elementInImport = content.querySelector('.myElement'); document.body.appendChild(elementInImport.cloneNode(true));
https://gist.github.com/shaunwallace/16418f9b255d448d9b34

It is also possible for one HTML import to also import another file. If you wanted to use or extend one component from within another you could. An example of this can be found here.

In addition to using sub-dependencies, HTML imports also allow for the caching of resources that are included in different imports. So for instance if one import uses jQuery as a dependency internally and then another resource is requested via importing then this asset will not be requested again. So if an import if requested multiple times and it in-turn requires certain dependencies, these will not be requested multiple times.

Imports do block rendering of the main page and this is similar to how stylesheet references block so to reduce FOUC. Because imports generally contain css style rules they also will need to be parsed and therefore will block page execution. There is a way to bypass this behavior by including the async attribute on the link to the import.

<link rel="import" href="/path/to/imported/assets.html" async>

Additionally, it’s good to know that imports don’t block parsing. Scripts inside of an import will be executed in the order in which they have been included. One method for optimizing async is to include your script references just below the closing <body> tag, which is a common best practice in front-end development. This way all scripts that are part of the HTML import are processed but do not block and all other javascript included in the page itself, either by reference or inline, will also not block rendering. More info on optimizing async behavior can be found here.

What is their support?

Currently there is not wide support for all of the features that make up web components. Chrome and Opera have full support for templates, HTML imports, custom elements, and shadow DOM. Firefox currently supports templates and has partial support for the rest. Safari > 7.1 has support for templates but no other support and IE supports none of the web components at the moment.

In order to use web components in all modern browsers one would have to look to Polymer.js. The library itself does support the entire suite of web component partials. If you’re looking for just custom elements then you can use X-Tags which is a set of polyfills maintained by Mozilla that uses a subset of the libraries provided by Polymer.js

--

--

Shaun Wallace

Senior Engineer @FanAI mostly playing around with javascript and the like.