Let’s Make a Web Component!
So Web Components – they’re hot like the centre of mince pies! Bet you want to take off the top, let the middle cool a bit, add some cream and start eating, right?
I know I do.
You may remember last year, when I wanted you to turn your phone off, close your twitter client, ignore your emails and take a couple of hours just for yourself. Well that time of year has come around again! So let’s do it, let’s make a Web Component :D
I’m pretty sure you’ve probably read about them this year, there’s also a big chance you’ve seen someone do a talk about them, (if you haven’t I highly recommend this wonderful talk by Peter Gasston), don’t worry if you haven’t though, I will do my best to explain each part as we go along – just so we’re all on the same page.
It worth noting this is an introduction, just a quick tutorial to give you a head start on all the shenanigans involved in Web Components. At the time of writing these examples only work in the latest version of Chrome (v31), I recommend caniuse.com for the latest and greatest browser support info.
You can find all the examples in this article here and all the code for them here.
So what is a Web Component. Well it’s a sort of sandboxed, reusable DOM structure. There are quite a few standards that go into making one. Take the logo for instance. Wouldn’t it be nice if that was interactive. It could be a working navigation for this site, you could hover and it could animate. Wouldn’t it be nice if you could hide away any crazy mark up you create for styling such a logo, if you could give the element a semantic name and wouldn’t it be nice if you could hand it all over for someone else to drop into their site, one component with it’s own HTML, CSS & JavaScript, which didn’t interrupt any of their existing code?
It would be very nice and entirely possible with all the standards that make up Web Components.
So let us start with the first nice thing. Let’s mark up and style that logo:
See the Pen 12 Devs Interactive Logo by Rumyra (@Rumyra) on CodePen
Templates
Templates are the first thing we should look at when trying to create a Web Component. To quote the working draft:
The
<template>
element contains markup intended to be used later.
You can put markup (or content) encapsulated by the <template>
tags anywhere in an HTML document. The content is parsed but is inert. Images aren’t downloaded, scripts aren’t passed etc… Pretty handy if we want to create a reusable/cloneable bit of markup.
<template id="twelve-devs-nav">
<div class="twelve-devs-nav">
<div class="badge-outer">
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
</div>
<nav>
<a href="/home">
<span>1</span>
<span>2</span>
<span>d</span>
<span>e</span>
<span>v</span>
<span>s</span>
<span>.</span>
<span>c</span>
<span>o</span>
<span>.</span>
<span>u</span>
<span>k</span>
</a>
<a href="/workshops">
<span>W</span>
<span>o</span>
<span>r</span>
<span>k</span>
<span>s</span>
<span>h</span>
<span>o</span>
<span>p</span>
<span>s</span>
</a>
<a href="/articles">
<span>A</span>
<span>r</span>
<span>t</span>
<span>i</span>
<span>c</span>
<span>l</span>
<span>e</span>
<span>s</span>
</a>
<a href="/events">
<span>E</span>
<span>v</span>
<span>e</span>
<span>n</span>
<span>t</span>
<span>s</span>
</a>
<a href="/talks">
<span>T</span>
<span>a</span>
<span>l</span>
<span>k</span>
<span>s</span>
</a>
</nav>
<div class="badge-inner">
<h1>12</h1>
</div>
</div>
</template>
You can access the markup to reuse or amend it via the new content
API.
var logoTemplate = document.querySelector('#twelve-devs-logo');
var logoContent = logoTemplate.content.cloneNode(true);
document.querySelector('#show-content').appendChild(logoContent);
All we’ve done here is taken the content out of the template, #twelve-devs-logo
, and stuck it into an empty div #show-content
, on the page to be rendered. Easy, but dare I say it handy.
Enter the Shadow Dom.
Hopefully you’ve got Chrome open by now. Go on inspect this audio element:
Can’t see anything but an empty audio tag right? Where is that play button, scrubber, volume control and time? It’s gotta be there somewhere…
Click on the cog icon to the bottom right of the dev tools. Under the Elements sub heading there is ‘Show Shadow DOM’ checkbox, select this and then close the Settings panel. Back in the dev tools you should now be able to expand the audio element. Keep expanding and a world of divs and inputs suddenly appear before you. All the controls are there, they are just hidden, inside the Shadow DOM.
So now you’ve seen the Shadow DOM, let’s use it! To hide away all my crazy markup I’ve used to create the 12 Devs Logo I can make the #show-content
div my Shadow DOM host, then populate it with the template’s content.
var logoTemplate = document.querySelector('#twelve-devs-logo');
var logoContent = logoTemplate.content.cloneNode(true);
var shadowHost = document.querySelector('#show-content');
var shadowRoot = shadowHost.webkitCreateShadowRoot();
shadowRoot.appendChild(logoContent);
It will then act like the <audio>
tag and not show the inner workings when inspected.
But wait up – where did all our styles go? Well the Shadow DOM comes with a Shadow Boundary. This means it automatically has scope, none of the global styles or scripts affect the local mark up and none of the local styles or scripts affect the rest of the page. (It’s worth noting you can reverse this with a little JavaScript if you so wish).
So let’s amend our template to include the styles we have externally in our style sheet. They still won’t affect anything because they are within the <template>
tag, which as we know from earlier are inert.
<template id="twelve-devs-nav">
<style>
div, nav, h1, audio {margin:0px;padding:0px;outline:none;border:none;font-family:sans-serif;}
.twelve-devs-nav {
position:relative;
width:300px; height:300px;
background-color:white;
}
.badge-outer {
width:300px; height:300px;
}
.badge-outer b {
display:block;
position:absolute; top:20px; left:50%;
width:150px; height:260px;
margin-left:-75px;
background-color:#4a4a4a;
transform-origin: center center 0 50%;
transform: rotate(0deg);
}
.badge-outer b:nth-of-type(2) {-webkit-transform: rotate(30deg);}
.badge-outer b:nth-of-type(3) {-webkit-transform: rotate(60deg);}
.badge-outer b:nth-of-type(4) {-webkit-transform: rotate(90deg);}
.badge-outer b:nth-of-type(5) {-webkit-transform: rotate(120deg);}
.badge-outer b:nth-of-type(6) {-webkit-transform: rotate(150deg);}
.badge-outer b:nth-of-type(7) {-webkit-transform: rotate(180deg);}
.badge-outer b:nth-of-type(8) {-webkit-transform: rotate(210deg);}
.badge-outer b:nth-of-type(9) {-webkit-transform: rotate(240deg);}
.badge-outer b:nth-of-type(10) {-webkit-transform: rotate(270deg);}
.badge-outer b:nth-of-type(11) {-webkit-transform: rotate(330deg);}
nav {
position:absolute; top:25px; left:25px;
background:white;
height:250px; width:250px;
border-radius:125px;
}
nav a {
font-size:0.9em; font-weight:bold;
text-transform: uppercase;
text-decoration: none;
color:#4a4a4a;
}
nav a:hover {font-size:1em; color:crimson;}
nav a span {
position:absolute; top:125px; left:117px;
height:15px; width:20px;
padding-top:94px;
text-align:center;
-webkit-transform-origin:top center;
}
nav a[href*='home'] {font-size: 0.7em;}
nav a[href*='home']:hover {font-size: 0.8em; color:crimson;}
nav a[href*='home'] span {
bottom:0px; right:0px;
padding-top:98px;
}
nav a[href*='home'] span:nth-of-type(1) {-webkit-transform:rotate(36deg);}
nav a[href*='home'] span:nth-of-type(2) {-webkit-transform:rotate(30deg);}
nav a[href*='home'] span:nth-of-type(3) {-webkit-transform:rotate(24deg);}
nav a[href*='home'] span:nth-of-type(4) {-webkit-transform:rotate(18deg);}
nav a[href*='home'] span:nth-of-type(5) {-webkit-transform:rotate(12deg);}
nav a[href*='home'] span:nth-of-type(6) {-webkit-transform:rotate(6deg);}
nav a[href*='home'] span:nth-of-type(7) {-webkit-transform:rotate(0deg);}
nav a[href*='home'] span:nth-of-type(8) {-webkit-transform:rotate(-6deg);}
nav a[href*='home'] span:nth-of-type(9) {-webkit-transform:rotate(-12deg);}
nav a[href*='home'] span:nth-of-type(10) {-webkit-transform:rotate(-18deg);}
nav a[href*='home'] span:nth-of-type(11) {-webkit-transform:rotate(-24deg);}
nav a[href*='home'] span:nth-of-type(12) {-webkit-transform:rotate(-30deg);}
nav a[href*='work'] span:nth-of-type(1) {-webkit-transform:rotate(300deg);}
nav a[href*='work'] span:nth-of-type(2) {-webkit-transform:rotate(292deg);}
nav a[href*='work'] span:nth-of-type(3) {-webkit-transform:rotate(284deg);}
nav a[href*='work'] span:nth-of-type(4) {-webkit-transform:rotate(276deg);}
nav a[href*='work'] span:nth-of-type(5) {-webkit-transform:rotate(268deg);}
nav a[href*='work'] span:nth-of-type(6) {-webkit-transform:rotate(260deg);}
nav a[href*='work'] span:nth-of-type(7) {-webkit-transform:rotate(252deg);}
nav a[href*='work'] span:nth-of-type(8) {-webkit-transform:rotate(244deg);}
nav a[href*='work'] span:nth-of-type(9) {-webkit-transform:rotate(236deg);}
nav a[href*='art'] span:nth-of-type(1) {-webkit-transform:rotate(220deg);}
nav a[href*='art'] span:nth-of-type(2) {-webkit-transform:rotate(212deg);}
nav a[href*='art'] span:nth-of-type(3) {-webkit-transform:rotate(204deg);}
nav a[href*='art'] span:nth-of-type(4) {-webkit-transform:rotate(196deg);}
nav a[href*='art'] span:nth-of-type(5) {-webkit-transform:rotate(188deg);}
nav a[href*='art'] span:nth-of-type(6) {-webkit-transform:rotate(180deg);}
nav a[href*='art'] span:nth-of-type(7) {-webkit-transform:rotate(172deg);}
nav a[href*='art'] span:nth-of-type(8) {-webkit-transform:rotate(164deg);}
nav a[href*='eve'] span:nth-of-type(1) {-webkit-transform:rotate(148deg);}
nav a[href*='eve'] span:nth-of-type(2) {-webkit-transform:rotate(140deg);}
nav a[href*='eve'] span:nth-of-type(3) {-webkit-transform:rotate(132deg);}
nav a[href*='eve'] span:nth-of-type(4) {-webkit-transform:rotate(124deg);}
nav a[href*='eve'] span:nth-of-type(5) {-webkit-transform:rotate(116deg);}
nav a[href*='eve'] span:nth-of-type(6) {-webkit-transform:rotate(108deg);}
nav a[href*='talk'] span:nth-of-type(1) {-webkit-transform:rotate(92deg);}
nav a[href*='talk'] span:nth-of-type(2) {-webkit-transform:rotate(86deg);}
nav a[href*='talk'] span:nth-of-type(3) {-webkit-transform:rotate(78deg);}
nav a[href*='talk'] span:nth-of-type(4) {-webkit-transform:rotate(70deg);}
nav a[href*='talk'] span:nth-of-type(5) {-webkit-transform:rotate(62deg);}
.badge-inner {
position:absolute; top:68px; left:68px;
height:165px; width:165px;
background: #4a4a4a;
border-radius:90px;
}
.badge-inner h1 {
padding:12px 0px 0px 15px;
font-size:7.6em;
color:white;
letter-spacing: -12px;
}
</style>
<div class="twelve-devs-nav">
<div class="badge-outer">
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
<b></b>
</div>
<nav>
<a href="/home">
<span>1</span>
<span>2</span>
<span>d</span>
<span>e</span>
<span>v</span>
<span>s</span>
<span>.</span>
<span>c</span>
<span>o</span>
<span>.</span>
<span>u</span>
<span>k</span>
</a>
<a href="/workshops">
<span>W</span>
<span>o</span>
<span>r</span>
<span>k</span>
<span>s</span>
<span>h</span>
<span>o</span>
<span>p</span>
<span>s</span>
</a>
<a href="/articles">
<span>A</span>
<span>r</span>
<span>t</span>
<span>i</span>
<span>c</span>
<span>l</span>
<span>e</span>
<span>s</span>
</a>
<a href="/events">
<span>E</span>
<span>v</span>
<span>e</span>
<span>n</span>
<span>t</span>
<span>s</span>
</a>
<a href="/talks">
<span>T</span>
<span>a</span>
<span>l</span>
<span>k</span>
<span>s</span>
</a>
</nav>
<div class="badge-inner">
<h1>12</h1>
</div>
</div>
</template>
So here we have it the logo is all Shadow DOM ready :)
Now all I feel like I need is some proper semantics for it. <div id="twelve-devs-logo">
doesn’t seem to cut it for me.
Bring on Custom Elements
In Chrome navigate to chrome://flags and enable Experimental Web Platform features, (remember to relaunch Chrome).
To create a Custom Element we need to register it via the document.register()
method, which takes two arguments, the name of the new element (which needs to contain a hyphen), and a prototype, (optional), which must extend HTMLElement
. So let’s create our prototype:
var twelveDevsNavProto = Object.create(HTMLElement.prototype);
This prototype comes with a createdCallBack
method, which we can use to do all our Shadow DOM population shenanigans from earlier.
twelveDevsNavProto.createdCallback = function() {
var shadowRoot = this.webkitCreateShadowRoot();
shadowRoot.appendChild(logoContent);
};
Now we can register a new element:
var twelveDevsNavElement = document.register('twelve-devs-nav', {prototype: twelveDevsNavProto} );
And include it in our mark up:
<twelve-devs-nav></twelve-devs-nav>
Well well, there you have it, a shiny new, you made it all, HTML element. Styleable, scriptable and reusable with the best of them :D
There’s so much more
I’ve only nibbled at the top of the mince pie so far, there’s so much more to it. If you’re excited and want to carry on here’s some further eating for the holidays:
Check out HTML Imports to help you port that Custom Element around the place. Also in a bid not to overwhelm you I didn’t touch on decorators, keep an eye out for them.
The the W3C Working Draft is super helpful for further learning, as is this article by Rob Dodson on CSS Tricks. As always HTML5 Rocks has a whole host of great articles & tutorials.
Both Mozilla and Google are building UI libraries based on web components. Mozilla have Brick and Google have Polymer, both are worth checking out.
And you’re done, pat yourself on the back, another thing explored and in your arsenal for the coming year!