Importing Code - Building HTML5 Web Components - Developing Web Components (2015)

Developing Web Components (2015)

Part III. Building HTML5 Web Components

Chapter 13. Importing Code

Jason Strimpel

Practically every other application platform allows packaging and importing code, but the web platform does not. This makes it extremely difficult to share code in a common fashion across applications. In the absence of a standard import, many other creative solutions have been invented and adopted by developers. However, none of these solutions have holistically addressed the need to include CSS, JavaScript, HTML, images, etc. as a single resource. This is what imports promise to deliver.

NOTE

There are imports already, in the sense that there are <link> tags for CSS and <script> tags for JavaScript that work for including code. However, these do not scale well or allow developers to define complete resources in a standard way that works consistently.

Declaring an Import

Importing code is as simple as adding a <link> and setting the rel value to import:

<head>

<link id="meeseeks-import" rel="import" href="/imports/meeseeks/index.html">

</head>

Imports can also be added to a document programmatically:

<head>

<script type="text/javascript">

var link = document.createElement('link');

link.rel = 'import';

link.id = 'meeseeks-import';

link.href = '/imports/meeseeks/index.html';

link.onload = function (e) {

// do something with import

};

link.onerror = function (e) {

// doh! something went wrong loading import

// handle error accordingly

};

document.head.appendChild(link);

</script>

</head>

NOTE

An import is only loaded and parsed once, based on the URL, regardless of how many times it is requested, so the imported JavaScript is only executed once.

In order to make use of the resources in an import—CSS, HTML, and JavaScript—the browser must support imports. Detecting support for imports can be done by checking for the import property in a <link>:

// determine if browser supports imports

var supportsImports = (function () {

return 'import' in document.createElement('link');

})();

Accessing an Import’s Content

The content of an import can be accessed by getting a reference to the import <link> and accessing the import property. Let’s use the source of the import referenced in the previous section, /imports/meeseeks/index.html, as an example:

<link rel="stylesheet" href="index.css" id="meeseeks-styles"></link>

<template id="meeseeks-template">

<p>I'm Mr. Meeseeks, look at me!</p>

</template>

<script type="text/javascript">

var MrMeeseeks = (function () {

'use strict';

// importer/parent document; more on this later

var parentDocument = document;

// import document; more on this later

var importDocument = parentDocument.currentScript.ownerDocument;

var template = importDocument.querySelector('#meeseeks-template')

.content;

function MrMeeseeks(el, options) {

var self = this;

this.options = options;

this.el = el;

// append template to parent el

this.el.appendChild(parentDocument

.importNode(template.content, true);

// set interval to check if task has been completed

this.isDoneInterval = setInterval(function () {

self.isTaskComplete();

}, this.interval);

}

MrMeeseeks.prototype.limit = 3600000;

MrMeeseeks.prototype.interval = 60000;

MrMeeseeks.prototype.taskComplete = false;

MrMeeseeks.prototype.isTaskComplete = function () {

if (this.taskComplete) {

clearInterval(this.isDoneInterval);

return;

}

alert('Existence is pain to a Meeseeks Jerry...' +

'and we will do anything to alleviate that pain!');

}

return MrMeeseeks;

})();

</script>

There is quite a bit of new material in the previous code block. Let’s break it down section by section. The MrMeeseeks constructor is handling all the resources in this import, but the parent document that imported the code can just as easily access the code as well.

Referencing Documents

You might have noticed parentDocument.currentScript.ownerDocument at the top of the immediately invoked function wrapper inside of the /imports/meeseeks/index.html import. parentDocument is a reference to document, which is the document that imported the import. document is always the top-level document, regardless of the context. The currentScript value is the “<script> element whose script is currently being processed,” and ownerDocument returns the top-level document object for a node. The combination of these two properties returns a reference to the import document.

An import document can be referenced from the main document by selecting the import node and accessing the import property:

var importDocument = document.querySelector('#meeseeks-import').import

Applying Styles

Styles are automatically applied to the main document when an import is loaded, so it is important to ensure that the import being loaded has properly namespaced its selectors. Styles can be removed by deleting the import node or by targeting <link> and <style> nodes in the import:

// remove the import

var link = document.querySelector('#meeseeks-import');

link.parentNode.removeChild(link);

// remove an import link node

var importDocument = document.querySelector('#meeseeks-import').import

var importLink = importDocument.querySelector('#meeseeks-styles');

importLink.parentNode.removeChild(importLink);

NOTE

If an import <link> with the same URL as a previously added import is added to the main document, it does not impact the CSS cascade. However, if an import <link> is removed from the main document and then an import <link> with the same URL as the removed import is added, it does impact the CSS cascade. The CSS cascade is also impacted if the import <link>nodes’ order is programmatically modified in the DOM tree.

The latter example is probably not that useful, but there are a couple of different use cases that make the former example of removing imports from the main document worthwhile. One use case is if you are using imports to apply themes to an application and the application allows users to toggle between themes (e.g., Gmail). A second use case is if you have a single-page application that lazy loads resources on a per-route basis, which requires adding and removing resources as a user navigates throughout the application.

NOTE

In a single-page application (SPA), either all necessary code—HTML, JavaScript, and CSS—is retrieved with a single page load, or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page, although modern web technologies (such as those included in HTML5) can provide the perception and navigability of separate logical pages in the application. Interaction with the SPA often involves dynamic communication with the web server behind the scenes.

The following example demonstrates the first use case, applying themes:

<!-- possible theme import examples to be loaded -->

<link data-theme="Stummies" rel="import" href="/imports/themes/stummies.html">

<link data-theme="GLeeMONEX" rel="import" href="/imports/themes/gleemonex.html">

// called on theme load

function onLoad(e) {

var theme = this.dataset.theme;

removeNodes('[data-theme]:not([data-theme="' + theme + '"])');

}

// called on theme load error

function onError(e) {

var theme = this.dataset.theme;

alert('Error loading the ' + theme + '!');

}

// used to remove previously loaded theme(s)

function removeNodes(selector) {

var nodes = document.querySelectorAll(selector);

[].forEach.call(nodes, function (node) {

node.parentNode.removeChild(node);

});

}

// called when user has selected a new theme in the UI

function setTheme(theme) {

var link = document.createElement('link');

link.rel = 'import';

link.dataset.theme = theme;

link.href = '/imports/themes/' + theme.toLowerCase() + '.html';

link.onload = onLoad;

link.onerror = onError;

document.head.appendChild(link);

}

Accessing Templates

An import is an excellent mechanism for loading templates in the main document. Import templates can be accessed just like <link> nodes:

var importDocument = document.querySelector('#meeseeks-import').import;

var template = importDocument.querySelector('#meeseeks-template');

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

document.querySelector('body').appendChild(templateClone);

One use case for importing templates is loading a commonly used set of templates, such as error pages that could be used by every route in a single-page application. For instance, if a route cannot locate a resource such as a controller object or there is an error loading the model data for the route, then it might be appropriate to respond with a 404-template or 500-template error:

<!-- import template content error -->

<template id="500-template">Something went terribly wrong.</template>

<template id="404-template">We can't find it.</template>

function onError(code, target) {

var errorDocument = document.querySelector('#errors-import').import;

var template = errorDocument.querySelector('#' + code + '-template');

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

target.appendChild(templateClone);

}

Executing JavaScript

JavaScript in an import is executed in the context of the main document, window.document. This is important because it means that any variables or functions defined within a <script> in an import, outside of a closure, will be part of the global context, window. This is useful for exporting functions to the main document, but at the same time it creates the potential for global name collisions, so use it with care!

NOTE

Removing an import <link> from the main document does not remove the JavaScript that was executed by the import—keep this in mind when loading imports.

Understanding Imports in Relation to the Main Document

It is important to remember that basic best practices still apply when dealing with imports—minimize network calls, minify files, place scripts at the bottom of the page, etc. In addition to performance considerations, though, there are other aspects that need to be understood when importing documents.

Parsing Imports

Imports do not block the parsing of the main document, which means that all import scripts are processed as if they had the defer property set. Import scripts are processed in order after the main document has finished executing any JavaScript in its <script> nodes and external JavaScript that is not marked as deferred. This non-blocking behavior is beneficial because it means imports can be placed at the top of the <head>, allowing the browser to begin downloading and processing the content as soon as possible without impacting the loading and rendering of the main document As a consequence, all performance best practices for improving the rendering time of a page—e.g., placing <script> nodes at the bottom—still hold true.

Cross-Domain Considerations

Import URLs are governed by the same rules as AJAX requests, so cross-origin requests are not allowed unless the server that is responsible for delivering the imports has been configured to support cross-origin resource sharing (CORS). These restrictions should be taken into consideration when configuring a server that will be used as an import repository for serving applications on different domains.

Subimports

Imports can import other imports. The rationales for this are the same as for modularizing other code: reuse, abstractions, testability, extendibility, etc. The same parsing and execution rules apply.

Loading Custom Elements

Up to this point we have evaluated imports for loading CSS, templates, and JavaScript, and have explored ways for accessing and managing these resources. Throughout the examples the onus has been on the parent document to leverage these resources. The MrMeeseeks import example managed some of the resources through an API it exposed. This API made the management of resources easier and limited the exposure of implementation details. However, it attached the MrMeeseeks object to window, which violates a JavaScript best practice: do not pollute the global namespace. In a small application this is less of a problem, but the larger an application becomes the more likely there is to be a name collision in window, which could cause an application error. In order to properly manage this, a registration system and a contract between the import and the main document would need to be defined. Fortunately, web components make this possible for us via custom elements:

<style>

sea-of-green:unresolved {

opacity: 0;

}

sea-of-green {

opacity: 1;

background: green;

border-radius: 50%

}

</style>

<template id="sea-of-green">

Hydrolate, verdant chrysoidine.

</template>

<script type="text/javascript">

document.registerElement('sea-of-green', {

prototype: Object.create(HTMLElement.prototype, {

createdCallback: {

value: function () {

var template = document.querySelector('#sea-of-green');

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

this.createShadowRoot().appendChild(content);

}

}

})

});

</script>

The import loads a custom element that is self-registering because the JavaScript is executed after the import content and the main document have been parsed. This makes the custom element available to the main document automatically. In addition to this, the custom element provides a commonly understood contract because it has a natively defined life cycle. This custom element can then easily be used in the main document:

<head>

<link rel="import" href="/imports/sea-of-green/index.html">

</head>

<body>

<sea-of-green />

</body>

Importing the Dialog

Including the dialog custom element source in an import is simply a matter of copying and pasting the source into an import file. Then any page that includes the import can easily use the dialog custom element:

<head>

<link rel="import" href="/imports/dialog/index.html">

</head>

<body>

<dialog-component title="After Ford">

Ending is better than mending. <br />

The more stitches, the less riches.

</dialog-component>

</body>

Summary

In this chapter we first introduced the premise for imports, then covered how to declare an import both programmatically and declaratively. Next we examined how to access an import’s content, which included referencing the import and main documents, accessing styles, leveraging templates, and understanding JavaScript execution in an import. After that we looked at how imports are processed by the main document. Then we covered using imports to load custom elements and the benefits that the self-registering pattern offers. Finally, we saw how to use imports to load the dialog custom element into the main document.