HTML5 components

Implement standard components

Using standard HTML5 APIs and open source polyfills

Content series: This content is part # of # in the series: HTML5 components Stay tuned for additional content in this series. This content is part of the series: HTML5 components Stay tuned for additional content in this series.

About this series A standard UI component model for HTML5 is still evolving. In this series, HTML5 expert David Geary shows how to create your own ad-hoc HTML5 components with existing technology — and how to leverage specifications that are on the way to defining a bona-fide HTML5 component system.

In the first two articles in this three-part series, you learned how to implement an ad-hoc HTML5 slider component. That component provides a lot of sophisticated functionality; for example, it uses CSS3 transitions to animate the slider's knob smoothly when the user clicks in the slider's rail. But (as you can see from the series' first article) from a developer standpoint, it's not as straightforward to use in an application as built-in HTML elements.

To use the ad-hoc slider, you must:

Add an empty DIV to your HTML. Create a new COREHTML5.Slider . Append the slider you created in Step 2 to the empty DIV you created in Step 1. Implement event handlers to wire controls to the slider.

Besides being difficult to use, the ad-hoc slider is also vulnerable to abuse, whether intentional or not. Anyone can access the slider's elements, because — like nearly all other JavaScript ad-hoc components in existence today — the slider adds them directly into the DOM tree.

Ad-hoc components An ad-hoc component is a component that does not adhere to a standard API for implementing components.

It would be ideal to reduce the preceding steps to simply Add a slider tag to your HTML. That's exactly what you'll do in this article by turning the ad-hoc slider into a standard HTML5 component, using the following APIs:

Shadow DOM : The Shadow DOM API enables you to add elements to the DOM tree that are inaccessible via the usual means, such as document.getElementById() . Elements in the Shadow DOM lurk in the shadows; they are effectively invisible to the rest of the DOM tree and by default, they are unaffected by CSS in the surrounding document.

: The Shadow DOM API enables you to add elements to the DOM tree that are inaccessible via the usual means, such as . Elements in the Shadow DOM lurk in the shadows; they are effectively invisible to the rest of the DOM tree and by default, they are unaffected by CSS in the surrounding document. Templates : An HTML5 template is an inert document fragment. When you need to display that fragment, you can stamp out (meaning, create) the template, causing the fragment to go live in the DOM tree. Templates let you encapsulate HTML, JavaScript, and CSS that you can stamp out and customize.

: An HTML5 template is an inert document fragment. When you need to display that fragment, you can stamp out (meaning, create) the template, causing the fragment to go live in the DOM tree. Templates let you encapsulate HTML, JavaScript, and CSS that you can stamp out and customize. Custom elements : Custom elements are tags for your components. They are indistinguishable from built-in HTML tags, with the exception that tags for custom components must contain a hyphen (for example, <custom-tag> ).

: Custom elements are tags for your components. They are indistinguishable from built-in HTML tags, with the exception that tags for custom components must contain a hyphen (for example, ). HTML imports: HTML imports let you import one HTML file from another, which enables you to implement custom components in their own files.

These APIs are still in development. At the time this article was written, no browsers supported them all. So to get started implementing HTML5 standard components today, you can choose from two open source projects that provide the missing functionality: Polymer and X-Tags.

This article shows you how to:

Encapsulate component markup in the Shadow DOM.

Stamp out live DOM elements from inert HTML templates.

Create custom elements and wire them to JavaScript.

Import HTML files.

Implement components with Polymer.

Implement components with X-Tags.

Chrome only At the time this article was written, the Shadow DOM examples worked only in the latest version of the Chrome browser.

Shadow DOM

Figure 1 shows an HTML5 application playing a video, along with Chrome's Elements inspector (available under Tools->Developer tools), which displays the elements inside the video element:

Figure 1. Looking inside the video element

Notice that the Elements inspector shows only a single source element inside the video element, even though it's obvious that the video element contains a play/pause button, progress indicator, and other elements. You don't see them because they reside in the Shadow DOM, which is invisible by default. Figure 2 shows how to display Shadow DOM elements in Chrome's Elements inspector:

Figure 2. Enabling shadow DOM in Chrome

To get to the settings dialog shown in Figure 2, click the gear icon in the bottom right corner of the Chrome developer tools window. Scroll down to the Show Shadow DOM check box. When the box is checked, Chrome's Elements inspector shows elements in the shadow DOM, as illustrated in Figure 3:

Figure 3. Looking at the video element's shadow DOM

Figure 3, like Figure 1, shows the elements inside the video element, but this time the video element's internals are visible because Chrome is displaying Shadow DOM elements.

Shadow DOM — until recently available only to browser implementers — turns out to be useful to application developers, because by default Shadow DOM elements are inaccessible to and unaffected by the surrounding DOM tree. Because they are essentially encapsulated in a world of their own, other components do not interfere with how they work.

Figure 4 shows the result of a contrived example that illustrates how to put elements into a Shadow DOM:

Figure 4. Changing a button's shadow root

The application starts out by looking like the screen capture on the left in Figure 4, which displays just a title and a button. If you click the button, the application replaces the text with a picture and a caption (shown in the screen capture on the right), which reside in the button's Shadow DOM.

Listing 1 shows the HTML for the application shown in Figure 4:

Listing 1. Populating a button's Shadow DOM

<!DOCTYPE html> <html> <head> <title>Shadow DOM</title> <style> button { font: 18px Century Schoolbook; border: thin solid gray; background: rgba(200, 200, 200, 0.5); padding: 10px; } </style> </head> <body> <h1>Shadow DOM</h1> <button id='button'>Click this button</button> <script> var b = document.getElementById('button'); b.onclick = function (e) { var sr = b.webkitCreateShadowRoot(); sr.innerHTML = '<p>This content is in the shadow DOM</p>' + '<img src="beach.png">'; }; </script> </body> </html>

When the application loads in a browser, the JavaScript at the bottom of Listing 1 runs, creating an onclick event handler that uses the webkitCreateShadowRoot() method to create a shadow root, meaning the top node in a Shadow DOM tree. The event handler then populates that shadow root with two elements: a paragraph and an image.

Shadow DOM provides encapsulation Shadow DOM provides the most basic tenet of object-oriented programming — encapsulation — for custom components. Component developers can use the Shadow DOM to protect the internals of their components from outside interference in the form of CSS styles or programmatic access. That capability is essential to ensure that components can safely interoperate.

Whenever you create a shadow root for an element and subsequently set the element's inner HTML, you override the element's original content, as the application in Figure 4 illustrates.

Buttons don't have a Shadow DOM by default, so the browser lets you create one, as illustrated in Listing 1. But for elements, such as the video element, that already have a Shadow DOM by default, the browser is not so accommodating. As shown in Figure 5, sr = b.webkitCreateShadowRoot(); causes the Chrome Elements inspector to issue an A Node was inserted somewhere it doesn't belong error message:

Figure 5. Trying (and failing) to change the video element's shadow root

Templates, custom elements, and HTML imports

Now that you've seen the Shadow DOM in action, I'll put it to pragmatic use along with the other staples of HTML5 web components: templates, custom elements, and HTML imports.

Figure 6 shows a simple unordered list of meetups at the HTML5 Denver Meetup group:

Figure 6. An unordered list of meetups

The markup that creates the unordered list in Figure 6 is shown in Listing 2:

Listing 2. HTML for an unordered list of meetups

<!DOCTYPE html> <html> <head> <title>Meetups Component</title> </head> <body> <ul> <li class="mobile">Sencha Touch. <span class='date'>Jan 23</span></li> <li class="html5-in-general">HTML5 & Semantic Markup. <span class='date'>Jun 18</span></li> <li class="html5-in-general">HTML5 & Windows 8. <span class='date'>Jul 23</span></li> <li class="javascript">HTML5 JavaScript on Crack. <span class='date'>Aug 20</span></li> <li class="javascript">CoffeeScript for Recovering JavaScript Programmers. <span class='date'>Sept 17</span></li> <li class="html5-in-general">ClojureScript and CouchDB. <span class='date'>May 21</span></li> <li class="html5-in-general">Polyfills for the Pragmatist. <span class='date'>Apr 23</span></li> <li class="design">CSS3 for Programmers. <span class='date'>Feb 20</span></li> <li class="lightning">Quick-start web apps; Graphics; Data visualization; Adaptive patterns; JSON. <span class='date'>Oct 22</span></li> <li class="lightning">Web workers; CSS3; HTTP; Audio, video, & canvas; Charting; JS evolution. <span class='date'>March 19</span></li> </ul> </body> </html>

Listing 2's HTML is unremarkable; it simply creates a single unordered list with list items. Figure 7 shows the same list items, but instead of residing in an unordered list, the items are inside of — and manipulated by — a custom component:

Figure 7. A custom meetup list component

Listing 3 shows the markup for the application shown in Figure 7:

Listing 3. A meetup-talks component instead of an unordered list

<!DOCTYPE html> <html> <head> <title>Meetups Component</title> <script src="../polymer/polymer.js"></script> <link rel="components" href="meetup-component.html"> </head> <body> <meetup-talks> <li class="mobile">Sencha Touch. <span class='date'>Jan 23</span></li> <li class="html5-in-general">HTML5 & Semantic Markup. <span class='date'>Jun 18</span></li> <li class="html5-in-general">HTML5 & Windows 8. <span class='date'>Jul 23</span></li> <li class="javascript">HTML5 JavaScript on Crack. <span class='date'>Aug 20</span></li> <li class="javascript">CoffeeScript for Recovering JavaScript Programmers. <span class='date'>Sept 17</span></li> <li class="html5-in-general">ClojureScript and CouchDB. <span class='date'>May 21</span></li> <li class="html5-in-general">Polyfills for the Pragmatist. <span class='date'>Apr 23</span></li> <li class="design">CSS3 for Programmers. <span class='date'>Feb 20</span></li> <li class="lightning">Quick-start web apps; Graphics; Data visualization; Adaptive patterns; JSON. <span class='date'>Oct 22</span></li> <li class="lightning">Web workers; CSS3; HTTP; Audio, video, & canvas; Charting; JS evolution. <span class='date'>March 19</span></li> </meetup-talks> </body> </html>

Notice that Listing 3's markup is nearly identical to Listing 2's, with three differences:

The markup in Listing 3 includes a file named polymer.js. That file comes with the Polymer open source project, which provides implementations of the HTML 5 Web Component APIs (Shadow DOM, templates, and so on). The Polymer project, which I discuss in more detail in the Polymer section next, is implemented by the Chrome development team.

Listing 3 uses a link element to import another HTML5 file. That use of the link element is known as an HTML import. At the time this article was written, HTML imports were not implemented by any browsers; in this example, that functionality comes from the Polymer project.

element to import another HTML5 file. That use of the element is known as an HTML import. At the time this article was written, HTML imports were not implemented by any browsers; in this example, that functionality comes from the Polymer project. The unordered list element in Listing 2 is replaced with a meetup-talks custom element in Listing 3.

The meetup-talks custom element is implemented in meetup-component.html, shown in Listing 4:

Listing 4. The meetup-list element

<element name="meetup-list"> <template> <style> /* styles that follow only apply to the ShadowDOM of the <meetup-list> element */ #meetups { display: block; padding: 15px; padding-top: 0px; background: lightgray; border: thin solid cornflowerblue; } .title { color: blue; font-size: 1.5em; } </style> <div id='meetups'> <p class='title'>HTML5 in General</p> <content select='.html5-in-general'></content> <p class='title'>JavaScript</p> <content select='.javascript'></content> <p class='title'>Design</p> <content select='.design'></content> <p class='title'>Lightning</p> <content select='.lightning'></content> </div> </template> <script> Polymer.register(this); </script> </element>

Listing 4 is where things get interesting. First, I declare a new element with the name meetup-list , being careful to include a hyphen in the name as required by the HTML specification. At the end of the listing, a single line of JavaScript registers the meetup-list element with the Polymer global object. That takes care of declaring and registering the custom element so it can be used in HTML pages.

Inside the element element, I declare a template . Templates define Shadow DOMs declaratively instead of programmatically. So in Listing 4, I create a Shadow DOM template. Whenever someone uses a meetup-list element in an HTML page, the browser uses that Shadow DOM template to stamp out (create) a new Shadow DOM instance for the element in question.

Inside the template, I define two CSS styles that apply only to the elements in the template. Recall that conversely — because a template represents a Shadow DOM — CSS styles defined in the surrounding document do not affect the elements in the template. For example, if you were to add a style for paragraph elements to the HTML in Listing 3, the style would not affect paragraphs in the template.

Besides CSS styles, the template also defines the elements in the ensuing Shadow DOM instances. The content element uses a CSS selector to select list items from original content of the meetup-list . So templates, in addition to declaratively specifying Shadow DOM instances, can also selectively insert an element's original content into the template itself. Note that you can use <content></content> to insert the entire original content of an element into its template.

Implementing web components now

All the Web Components APIs that I discuss in this article are relatively new, with varying degrees of support among browser vendors. To implement components effectively today, you can't just write code and load it into your browser; instead, you need what's known in the HTML5 business as polyfills or shims : solutions that let you use new functionality if it's available but fall back to a supported alternative when it's not.

Currently, two projects provide Web Components polyfills: Polymer and Mozilla X-Tags. Polymer only works with Chrome and requires a web server to use HTML imports, making it unsuitable for production use. X-Tags works with nearly all modern browsers and doesn't require a web server; however, X-Tags only implements the custom-elements API, leaving out templates and Shadow DOM.

Next, I show you how to use Polymer and X-Tags, respectively, to wrap a standard component around the ad-hoc slider that I implemented in the previous articles in this series.

Polymer

By definition, ad-hoc components don't adhere to a component standard, so there's no standard way to implement them or use them. But you can make them easier to use by wrapping them in standard components. First, I use Polymer to wrap the ad-hoc slider in a standard component. Figure 8 shows the Polymer version of the slider:

Figure 8. A polymer slider

Listing 5 shows how to use the Polymer version of the slider:

Listing 5. Using the Polymer slider tag

<!DOCTYPE html> <html> <head> <title>Slider with Polymer</title> <script src="../polymer/polymer.js"></script> <script src="lib/slider.js"></script> <link rel="import" href="./slider.html" /> </head> <body> <x-slider id='slider' fillColor='goldenrod' strokeColor='red'> </x-slider> </body> </html>

If you compare Listing 5 to Listing 2 in the first article in this series, which shows how to use the ad hoc slider by itself, you can see that Listing 5 is strikingly more simple. With the x-slider Polymer element, you can create a slider with one line of HTML.

The x-slider element is implemented in Listing 6:

Listing 6. The Polymer slider component

<element name="x-slider" attributes="strokeColor fillColor"> <template> <style> x-slider { display: block; } .x-slider-button { float: left; margin-left: 2px; margin-top: 5px; margin-right: 5px; vertical-align: center; border-radius: 4px; border: 1px solid rgb(100, 100, 180); background: rgba(255, 255, 0, 0.2); box-shadow: 1px 1px 4px rgba(0,0,0,0.5); cursor: pointer; width: 25px; height: 20px; } .x-slider-button:hover { background: rgba(255, 255, 0, 0.4); } #xSliderDiv { position: relative; float: right; width: 80%; height: 65%; } </style> <div id='x-slider-buttons-div' style='width: {{width}}px; height: {{height}}px;'> <button class='x-slider-button' id='xSliderMinusButton'>-</button> <button class='x-slider-button' id='xSliderPlusButton'/>+</button> <div id='xSliderDiv'></div> </div> </template> <script> Polymer.register(this, { width: 350, height: 50, strokeColor: 'blue', fillColor: 'cornflowerblue', ready: function () { var self = this, slider = new COREHTML5.Slider(this.strokeColor, this.fillColor, 0); setTimeout( function () { // This timeout is a hack slider.appendTo(self.$.xSliderDiv); slider.draw(); }, 200); this.$.xSliderMinusButton.onclick = function () { if (slider.knobPercent >= 0.1) { slider.knobPercent -= 0.1; slider.erase(); slider.draw(); } }; this.$.xSliderPlusButton.onclick = function () { if (slider.knobPercent <= 0.9) { slider.knobPercent += 0.1; slider.erase(); slider.draw(); } }; } }); </script> </element>

Listing 6 is similar to Listing 4. Both listings define a new element and register the element with the Polymer global object. Both also define a template containing CSS styles and markup. What distinguishes Listing 6 from Listing 4 is Listing 6's use of Polymer data binding and a ready event handler.

The double-mustache notation — as in {{width}} — denotes an element property. You can use double-mustache notation in your element's markup. In Listing 6, I set the width and height of the element's enclosing DIV to the width and height that the page author set with element properties.

You declare properties in an object that you pass to the Polymer.register() method; for example, Listing 6 declares four properties for the x-slider element: width , height , strokeColor , and fillColor . If you specify properties with the attributes attribute of the element element — known as publishing properties — page authors can use those properties in their elements, as is the case for the x-slider element in Listing 5.

According to the Polymer documentation, when a component has finished initializing itself, it calls its ready() method, if it exists. The documentation does not elaborate further about exactly when components initialize themselves, but evidently it's before the browser is ready to draw the component. Because components are initialized before they're ready to draw, and because the ready() method is the only method in the lifecycle at the moment, I had to add a setTimeout() hack that draws the slider 200ms after the ready() method is invoked. Rough edges like this will undoubtedly smooth out over time. (The Web Components specification also defines an inserted callback that the browser calls after it inserts an element into the DOM, but that was not supported by Polymer at the time this article was published.)

Polymer also provides a $ attribute on custom elements that references a map of the element's attributes. I use that map to access the DIV that ultimately contains the ad-hoc slider.

X-Tags

The X-Tags version of the slider is shown in Figure 9:

Figure 9. An X-Tags slider component

The Polymer and X-Tags implementations of the slider are visually indistinguishable; I implemented them with different colors merely to illustrate that they were implemented with different frameworks. Listing 7 shows how the application shown in Figure 9 uses the X-Tags version of the slider (also implemented as x-slider ):

Listing 7. Using the X-Tags slider component

<!DOCTYPE html> <html> <head> <title>Slider with x-tag</title> <link rel="stylesheet" type="text/css" href="x-slider.css" /> </head> <body> <x-slider id='slider' slider-fill-color='rgba(50, 105, 200, 0.8)' slider-stroke-color='navy'> </x-slider> </body> <script type="text/javascript" src="lib/x-tag.js"></script> <script type="text/javascript" src="lib/slider.js"></script> <script type="text/javascript" src="x-slider.js"></script> </html>

Listing 7 is similar to Listing 5. Both listings use a simple x-slider element to put a slider in the page. The difference is that the Polymer version uses HTML imports to include an HTML fragment that defines the slider, whereas the X-Tags version includes JavaScript instead because it does not support HTML imports.

The JavaScript that implements the component is shown in Listing 8:

Listing 8. The slider's JavaScript

function getFirstAncestor(name, startingElement) { var element = startingElement , localName = element.localName; while (localName !== name) { element = element.parentNode; localName = element.localName; } return element; }; function getSlider(element) { return getFirstAncestor('x-slider', element).slider; }; xtag.register('x-slider', { onCreate: function () { var content = "<div id='x-slider-buttons-div'>" + "<button class='x-slider-button' id='x-slider-minus-button'>-</button>" + "<button class='x-slider-button' id='x-slider-plus-button'>+</button>" + "</div>" + "" + "<div id='x-slider-slider-div'></div>" + "" + "<div id='x-slider-readout-div'></div>"; stroke = this.getAttribute('slider-stroke-color'), fill = this.getAttribute('slider-fill-color'); // 'this' is the x-slider HTML element this.max = 100; this.innerHTML = content; this.slider = new COREHTML5.Slider(stroke, fill, 0); this.slider.appendTo('x-slider-slider-div'); this.slider.draw(); }, events: { 'click:touch:delegate(#x-slider-plus-button)': function(event, slider) { var slider = getSlider(this) // 'this' is the button , value = getFirstAncestor('x-slider', this).getValue(); if (slider.knobPercent <= 0.9) { slider.knobPercent += 0.1; } slider.erase(); slider.draw(); console.log(value); }, 'click:touch:delegate(#x-slider-minus-button)': function(event, slider) { var slider = getSlider(this) // 'this' is the button , value = getFirstAncestor('x-slider', this).getValue(); if (slider.knobPercent >= 0.1) { slider.knobPercent -= 0.1; } slider.erase(); slider.draw(); console.log(value); }, }, methods: { getValue: function () { // 'this' is the x-slider HTML element return this.slider.knobPercent * this.max; } } });

Defining the component with X-Tags in Listing 8 is quite different from defining it with Polymer in Listing 6. X-Tags defines elements purely in JavaScript, which involves using string concatenation and setting an element's inner HTML to set the content for the custom element. That's in striking contrast to Polymer, which defines custom element content in much more readable and maintainable HTML.

X-Tags does not have an attribute like Polymer's $ that makes it easy to access elements within the custom element, so I had to implement a getFirstAncestor() function to access the element that houses the ad-hoc slider.

Finally, X-Tags provides support for implementing events to handle clicks on the buttons that control the slider's knob; however, for this exercise, that support seemed like overkill to me.

For completeness, the CSS for the X-Tags version of the slider is shown in Listing 9:

Listing 9. The slider's CSS

x-slider { display: block; } .x-slider-button { float: left; margin-left: 2px; margin-top: 15px; margin-right: 5px; vertical-align: center; border-radius: 4px; border: 1px solid rgb(100, 100, 180); background: rgba(255, 255, 0, 0.2); box-shadow: 1px 1px 4px rgba(0,0,0,0.5); cursor: pointer; width: 25px; height: 20px; } .x-slider-button:hover { background: rgba(255, 255, 0, 0.4); } #x-slider-buttons-div { width: 25%; height: 100%; } #x-slider-slider-div { position: relative; float: right; margin-top: -40px; width: 80%; height: 65%; }

Conclusion

Currently, JavaScript frameworks implement mostly ad-hoc components that adhere to proprietary APIs instead of standards. As a result, you face a considerable learning curve whenever you move to a new framework. And because ad-hoc components often are not encapsulated, their functionality can be adversely affected by other components in an application.

In this article series, you've seen how to use HTML5 APIs to implement both standard and ad-hoc components. The HTML5 specifications for implementing components are relatively new, with varying support among browser vendors. You can start implementing standard components today, but you need more than your browser to do so. In this article, I briefly discussed two frameworks, Polymer and X-Tags, that make it possible to implement standard components.

As the Web Components specifications mature and browser vendors implement their functionality, we'll soon have a viable platform on which to build standard components. Then, implementing HTML5 applications will be even more fun than it is today.

Downloadable resources

Related topics