Utilizing Templates - Building HTML5 Web Components - Developing Web Components (2015)

Developing Web Components (2015)

Part III. Building HTML5 Web Components

Part I of this book provided an understanding of how a browser lays out a page by introducing normal flow, positioning, and stacking contexts. Part II described how to use this understanding to manipulate the properties that affect the aforementioned topics to control a page’s layout. This knowledge was then used to create small widgets with specific purposes that could be combined to create a larger UI widget, the dialog example. However, these widgets are not web components as described by the W3C. They were designed to function within the current context of the Web. Part III is all about true web components and the benefits they offer. It will cover the parts that comprise the W3C Web Components specification, templates, custom elements, the shadow DOM, and imports. As part of this learning process, in this section of the book we will take the dialog widget from Parts I and II and convert it to a bona fide web component, making it more semantic, declarative, encapsulated, consumable, and maintainable. More importantly, here we will truly extend the web platform for the first time.

Chapter 10. Utilizing Templates

Jason Strimpel

As you read the title of this chapter you might have thought to yourself, “But we already have client-side templates for rendering markup!” Here’s an example:

<script id="some-template" type="text/x-handlebars-template">

<p class="description">{{description}}</p>

</script>

<div id="some-template" style="display: none;">

<p class="description">{{description}}</p>

</div>

function (Handlebars,depth0,helpers,partials,data) {

this.compilerInfo = [4,'>= 1.0.0'];

helpers = this.merge(helpers, Handlebars.helpers); data = data || {};

var buffer = "", stack1, helper, functionType="function",

escapeExpression=this.escapeExpression;

buffer += "<div id=\"some-template\" style=\"display: none;\">";

buffer += "<p class=\"description\">";

if (helper = helpers.description) {

stack1 = helper.call(depth0, {hash:{},data:data}); }

else { helper = (depth0 && depth0.description);

stack1 = typeof helper === functionType ?

helper.call(depth0, {hash:{},data:data}) : helper; }

buffer += escapeExpression(stack1)

+ "</p></div>";

return buffer;

}

While these approaches allow you to utilize templates on the client, they have their drawbacks. This is because they are really just workarounds designed to implement a missing feature: native templates in the browser. The good news is that this feature is no longer missing. It is now a W3C specification and is running in some of the latest browser versions. This new specification will allow you to begin writing native templates in a standardized fashion that are specifically designed to work more efficiently with the browser.

NOTE

Eric Bidelman has created a significant amount of the web component content on the Web outside of the W3C specifications. Eric’s postings and the W3C specifications were the primary sources for the majority of Part III, including this chapter. A big thanks to Eric and the many others who have been posting and speaking on this topic for helping to drive our learning.

Understanding the Importance of Templates

Many different template solutions currently exist in the market: Handlebars, Dust, Nunjucks, micro templating, etc. While these are useful and amazing solutions, they are only templates in a limited sense. They provide functionality similar to their server-side counterparts, compiling strings to functions. These functions then take data and output strings comprised of the original strings with the data substituted per the template tokens and processing rules. This functionality is extremely important, but the browser is a much different environment than the server. Since it is a different environment, it requires different features and optimizations.

NOTE

Native templates are not a replacement for existing template solutions. They are just a standardized way to get a chunk of markup to the browser and have it remain inert until it is needed. Per the W3C Web Components specification, “The <template> element has a property called content which holds the content of the template in a document fragment. When the author wants to use the content they can move or copy the nodes from this property.”

Deferring the Processing of Resources

One of the optimizations that templates afford is the deferment of the processing of assets such as scripts and images. This delay of processing allows developers to include as many templates as they want with virtually no impact on the rendering of the page (although transferring larger files over the network will, of course, affect the time to render).

Deferring the Rendering of Content

In addition to deferring the processing of assets, template markup is not rendered by the browser, regardless of where the template is located. This allows a developer to place templates in any location and conditionally render them without the need to toggle display properties or be concerned about the overhead of parsing markup not used by the browser.

Hiding the Content from the DOM

The template content is also not considered to be part of the DOM. When a query is made for DOM nodes, none of the template child nodes are returned. This ensures that templates do not slow down node lookups and that template content remains hidden until it is activated.

Creating and Using a Template

A template is created by adding a <template> to the markup, selecting the template node, cloning its content, and then inserting the cloned content into the DOM.

Detecting Browser Support

In order to make use of native templates, the first step is to determine if a browser supports them. This check can then be used to implement a polyfill to support templates in older browsers:

var supportsTemplates = (function () {

return 'content' in document.createElement('template');

})();

Placing a Template in Markup

A template is created by adding a <template> tag to a <head>, <body>, or <frameset>, or any descendant tag of the aforementioned tags:

<head>

<template id="atcq">

<p class="response"></p>

<script type="text/javascript">

(function () {

var p = confirm('You on point Tip?');

var responeEl = document.querySelector('.response');

if (p) {

responeEl.innerHTML = 'All the time Phife';

} else {

responeEl.innerHTML = 'Check the rhyme y\'all';

}

})();

</script>

</template>

</head>

Adding a Template to the DOM

To add a template to the DOM and render it, you must first get a reference to the template node. Then you need to make a copy of the node, and finally add the new node to the DOM:

// get a reference to the template node

var template = document.querySelector('#atcq');

// clone it

var templateClone = document.importNode(template.content, true);

// append the cloned content to the DOM

document.body.appendChild(templateClone);

Converting the Dialog Component to a Template

Converting the dialog component to a template is done by taking the JavaScript, CSS, and HTML required to render the dialog, and embedding it inside a <template> tag. This will make the JavaScript and CSS inert until the template is cloned and added to the DOM. Our dialog template looks like this:

<head>

<script type="text/javascript" src="/vendor/jquery.js"></script>

<template id="dialog">

<style>

// styling src

</style>

<script type="text/javascript">

// dialog component source

</script>

<div role="dialog" aria-labelledby="title" aria-describedby="content">

<h2 id="title">I am a title</h2>

<p id="content">Look at me! I am content.</p>

</div>

</template>

</head>

This template injects the inert resources into the DOM, but it does not provide a convenient API for developers to leverage the dialog component. If the template and the dialog component it contains is to be reused, then an API for accessing the dialog component must be created.

WARNING

You may have noticed that the template is embedding scripts directly as opposed to using the src attribute to import them. That is because any script added to the DOM after the document has been parsed loads sources asynchronously. Per the W3C HTML5 specification, one of the pieces of state associated with a script element is “a flag indicating whether the element will ‘force-async’. Initially, script elements must have this flag set. It is unset by the HTML parser and the XML parser on script elements they insert.” If the scripts were not embedded a number of possible errors could occur, as the resources would not be available until the scripts had completed loading.

Creating a Wrapper API for the Dialog Template

If you want to encapsulate cloning and appending the template, then it is best to create a wrapper constructor function that encapsulates this logic along with the dialog component instantiation. This encapsulation also allows you to embed optimizations such as only cloning the template once:

function DialogTemplate(options) {

this.querySelector = '[role="dialog"]';

this.template = document.querySelector('#dialog');

this.el = document.querySelector(this.querySelector);

this.options = options;

// only import and append once

if (!this.el) {

this.clone = document.importNode(this.template.content, true);

document.body.appendChild(this.clone);

this.el = document.querySelector('[role="dialog"]');

}

// create dialog instance

this.options.$el = this.querySelector;

this.api = new Dialog(this.options);

// set the title and content for the dialog

this.api.$el.find('#title').html(this.options.title);

this.api.$el.find('#content').html(this.options.content);

return this;

}

Instantiating a Dialog Component Instance

The dialog component can now be instantiated as before, but now it will be done from the template using the new wrapper constructor function:

var dialog = new DialogTemplate({

draggable: true,

resizable: true,

});

dialog.api.show();

Abstracting the Dialog Template Wrapper

The dialog component wrapper constructor function was specific to the dialog component. If you would like to create a more generic wrapper, it is possible. The trade-offs are that all your templates have to expose the same public API with the same signature, and you cannot encapsulate optimization logic inside of the wrapper constructor function. A generic template wrapper might look like the following:

// assumes that all templates return a public

// API that has the same signature

var templateFactory = (function (window) {

'use strict';

return function (template, API) {

function Template(options, callback) {

var self = this;

this.options = options;

// clone template content

this.clone = document.importNode(this.template.content, true);

// append cloned template content to target el

options.target.appendChild(this.clone);

// get el for public API exposed by appending template

this.el = document.querySelector(options.$root);

options.$el = this.el;

// assumes that template adds public API

// to the window and that API uses a

// constructor to create new instance

self.api = new window[self.API](options);

return this;

}

TemplateAPI.prototype.template = template;

TemplateAPI.prototype.API = API;

return Template;

};

})(window);

Summary

In this chapter we covered web component templates as defined by the W3C. First, we discussed the benefits these templates afford over the current template solutions (similar to their server-side counterparts):

§ The processing of resources such as images and tags is deferred, so they do not impact performance in that respect (any additional content sent across the network always decreases performance).

§ The rendering of content is deferred; content is not rendered unless it is explicitly added to the DOM.

§ Template content is hidden from the DOM, so it does not slow down query selections.

Then we created a template and imported it into the DOM. Next, we took the dialog component and made it into a template. Finally, we created a wrapper for instantiating dialog components from the dialog component template node and examined converting the dialog component template into a factory function that could be used to wrap any template that returned an API that subscribed to the contract defined in the template wrapper code.