Exploring HTML Imports
Web Components have come a long way in the past few months and one of the technologies that I’m most interested in is HTML Imports (or “imports”, for short). Imports allow you to load additional documents into your page without having to write a bunch of ajax. This is great for Custom Elements where you might want to import a suite of new tags. I spent the day playing with imports and thought it would be useful to write up my progress.
The Lowdown
Imports are a new type of link
tag which should be familiar to you since that’s also how we load our stylesheets.
For an import we just replace the rel
with one of type import
.
Support
Native imports are currently only available in Chrome. Be sure to check the support brackets on caniuse to see if things have changed since the writing of this articel. Thankfully Polymer offers a polyfill in its platform.js
file, if you want to try them out in other modern / “evergreen” browsers.
Codez!
I’ve created a sketchbook for this post and future Web Components related stuff. You can grab the sketchbook on GitHub. For each of the examples that I cover I’ll link to the sketch so you can quickly try things out.
Note: I’m in the process of updating this sketchbook. Please file an issue on Github if something seems broken.
A Basic Example
Sketch 0: Basic
OK so what’s a very basic import look like?
In its simplest form the import works just like calling a stylesheet. We have to make sure there’s a document to import so let’s create a fake blog post in imports/blog-post.html
.
To test, you’ll need to host your index.html
and imports/
folder on a local server. I recommend serve if you don’t already have one installed.
Once you have that setup visit your index page. If you take a look at the console you can see the request returning.
Well that’s cool, but now what?
Let’s grab the content of the import using some JavaScript and append it to the body. We’ll do this back in our index.html
.
First we query the link
tag which loaded our import. Then we extract our #blog-post
element and store it in a variable called content
. You’ll notice that we don’t have to write any event handler code to wait till the import has loaded, we can just assume the content is there and start working with it. Finally we add the new content to our body
.
If you’re following along you should end up with something like this:
Exciting, I know ;) But it demonstrates a no frills approach to loading content that doesn’t require ajax and writing our own event handlers. Let’s keep digging to see what else we find…
A Basic Example with Polymer
Sketch 1: Basic-Polymer
If you want to try out the snippets above in a browser other than Chrome you’ll need to use Google’s Polymer Project. Polymer is a collection of polyfills and additional sugars which seeks to enable the use of Web Components in all modern browsers. The hope is that devolopers will use Polymer to inform the W3C on which direction to take with Web Components; so rather than wait for a stinky spec we can guide the implementation process.
Polymer attempts to keep parity with the the evolving specifications but obviously there are some places where the API must differ because of the limitations of current browsers. In the case of HTML Imports, Polymer waits for the DOMContentLoaded
event before triggering the actual import process. This means we need to listen for the HTMLImportsLoaded
event on either window
or document
to know when it is finished. Let’s add that to our index.html
.
Using the above we should get the same results as before.
You might notice that I used platform.js
instead of only including the HTML Imports polyfill. Polymer’s collection of polyfills, referred to as “The Platform,” is structured so you can take any of the polyfills à la carte but I find it’s easier to just include the entire platform when I’m experimenting, rather than worry if I have each individual polyfill loaded. That’s just personal preference (a.k.a. I’m lazy).
Using Scoped Styles in our Imports
04/30/14 Note: It’s my understanding that scoped styles are being removed in favor of Shadow DOM so I’m removing this section
Using Scripts in our Imports
Sketch 3: Script
Next let’s look at using <script>
tags inside of our import. We’ll start by removing the <script>
block from our index.html
.
Then we’ll transfer that script block over to our blog post in imports/blog-post.html
.
If we run this we should get the exact same outcome as before.
An important thing to take notice of is the relationship between thisDoc
and thatDoc
. thisDoc
refers to the blog-post.html
document, while thatDoc
refers to our index.html
file. It’s useful to distinguish between the two so we can querySelector
for #blog-post
and not worry that we may have grabbed something out of the importing document. Thanks to Dominic Cooney for the heads up on this.
You’ll also notice that since the import has access to our document
object it is able to add itself to the page. In practice you probably wouldn’t want imports adding themselves wherever, but the important takeaway is that anything imported can access the document
. This means an import could register itself as a Custom Element using our document
object and we wouldn’t need to write any additional code. We’re almost to that point so let’s keep going…
Using Templates in our Imports
Sketch 4: Template
I’m getting a little tired of our fake “blog post” so let’s switch over to something more practical. We’ll use Chart.js to create a very simple pie diagram and we’ll use the new <template>
tag to hold the contents of our import. If you haven’t heard of the template tag before checkout this introduction.
To start, I’ve updated the index.html
so it includes Chart.js and imports a new chart.html
file.
Here’s what imports/chart.html
looks like:
We’re creating a new <template>
which contains a canvas tag and a script block to create our pie chart. The advantage of using a template tag is that any script blocks inside of it will not execute until we clone the contents and add them to the DOM.
Running the above gives us this:
Well this is interesting. We’re importing an entire pie chart and our index page isn’t cluttered with a bunch of code. Unfortunately we don’t have much control over where the pie chart ends up. It would be nice if we could turn the contents of the import into a tag and place that wherever. Thankfully Custom Elements let us do just that!
Using Custom Elements in our Imports
Sketch 5: Custom Element
I’ll say in advance that you might need to read through this section a few times before you fully grok it. We’re going to touch on a lot of new stuff so consider this the bonus round :)
The final markup for our index.html
file is going to look like this:
We’re going to use our new Custom Element, chart-pie
, which will allow us to produce pie charts wherever we want. The result will look like this:
Obviously not the most amazing thing ever but from a practical perspective being able to drop a pie chart on your page with one line of HTML is pretty sweet.
To create the chart-pie
tag we’ll need to create a Custom Element. Custom Elements are new tags with a lifecycle of JavaScript callbacks. Typically they use Shadow DOM to hide their internal markup and expose attributes and specific styles to the client. I wrote an article loosely explaining them a while back so take a look at that and also checkout this talk by Eric Bidelman.
Here’s what our updated imports/chart.html
looks like.
Let’s walk through it piece by piece.
<template id="chart-pie">
<canvas class="myChart" width="200" height="200"></canvas>
</template>
On lines 1-3 we’ve shortened the template
down so that it only contains our canvas
tag. We’ll use the Custom Element createdCallback
to actually instantiate the chart in here.
// thisDoc refers to the "importee", which is chart.html
var thisDoc = document.currentScript.ownerDocument;
// thatDoc refers to the "importer", which is index.html
var thatDoc = document;
var template = thisDoc.querySelector('#chart-pie');
Lines 6-12 should look familar from the last example. We’re storing our two documents in variables and querying for the template tag.
var ChartPieProto = Object.create(HTMLElement.prototype);
On line 15 we define the prototype for our Custom Element called ChartPieProto
. This prototype extends the HTMLElement
prototype which is a requirement for creating a new element.
ChartPieProto.createdCallback = function() {
...
};
On line 20 we see the first lifecycle callback, createdCallback
. The createdCallback
is run every time the parser hits a new instance of our tag. Therefore we can use it as a kind of constructor to kickoff the creation of our chart. We’ll want to create a new chart instance for each tag so all of our Chart.js code has been moved inside of this callback.
var root = this.createShadowRoot();
var clone = thatDoc.importNode(template.content, true);
On lines 22-23 we create a Shadow DOM to hold the markup for our chart.
var data = [
{
value: 30,
color:"#F38630"
},
{
value : 50,
color : "#E0E4CC"
},
{
value : 100,
color : "#69D2E7"
}
];
//Get the context of the canvas element we want to select
var ctx = clone.querySelector('.myChart').getContext('2d');
var myNewChart = new Chart(ctx).Pie(data);
Lines 26-43 should look familiar. It’s the same Chart.js code from before except now we use querySelector
to find the contents of the template clone and we’re using a class for myChart
instead of an id.
root.appendChild(clone);
On line 46 we add the new content to our Shadow DOM.
var ChartPie = thatDoc.registerElement('chart-pie', {prototype: ChartPieProto});
Line 49 is where we actually register our Custom Element and assign it to the name chart-pie
. From here you can either place a <chart-pie></chart-pie>
tag somewhere on your page, or use JavaScript to instantiate an instance and add it to the document
. This is demonstrated in the comments on lines 50-51. If you refer back to our index.html
example we just use the <chart-pie>
tag.
Which produces this:
Conclusion
If you’ve made it this far congrats and thanks for hanging in there! I know that last section was a little crazy but stop for a moment and think about what we just did.
By using an HTML Import we were able to pull in a document which added a new tag to our application. Imagine if all of Chart.js was written in this manner. There would be no need for us to write any glue code to generate a chart ever again. That would allow us to focus only on the code that matters to our application, and leave all that other boilerplate tucked away inside of Custom Elements.
Over the next few months I’ll be blogging exclusively about this topic because I think it’s really interesting so check back later for more!
Till then make sure to hit me up on Twitter if you have any questions or leave a note in the comments. Thanks!