My First Web Component

If you're like me, you've probably heard a lot about Web Components^tm over the past few years, but if you've never created one yourself, I'll show you how.

As you probably know, the Web Component spec is a beast. It encompasses the Shadow DOM, Custom Elements, HTML Imports, and Templates. All these specs combine like Voltron to make Web Components (but unlike Voltron, these specs are currently only work in Firefox and Chrome).

Our First Web Component

We'll be making a doge button like the one pictured above. At the end of this tutorial, you'll be able to use this component by inserting a <button is="doge-button">so click</button> element on your page. If you don't want to extend the <button> element, you'll be able to insert the doge button with the cooler <doge-button>so click</doge-button> syntax.

The CSS and HTML we'll be using for our doge-button looks like this:

Our CSS:

.doge-button{
    position: relative; 
    display: inline-block; 
    font-family: "Comic Sans MS", 'Comic Sans', "Marker Felt";
    cursor: pointer; 
}

.doge-face {
    background-image:   url('https://dl.dropboxusercontent.com/u/19492365/doge.png');
    height: 80px; 
    background-size: contain;
    background-repeat: no-repeat; 
    background-position: left center; 
}

.doge-button:hover .doge-txt{
    transform: rotate(-10deg); 
    color: yellow; 
    text-shadow: 2px 2px 0 magenta; 
}

.doge-txt{
    text-shadow: 0 0 0 #F0F;
    font-weight: bold;
    position: relative;
    margin-left: 30px;
    margin-bottom: 0px;
    margin-top: -30px;
    font-size: 18px;
    color: #F0F;
    transition: transform 0.1s ease-in, color 0.1s ease-in, text-shadow 0.1s ease-in;
    display: block;
}
.doge-button:hover:after{
    content: "WOW,"; 
    font-size: 18px;
    display: block; 
    position: absolute;
    top: 15px; 
    right: 0px; 
}

Nothing out of the ordinary here.

Our HTML

<div class="doge-button">
    <div class="doge-face"></div> 
    <div class="doge-txt">
        <content></content>
    </div> 
</div> 

Pretty ordinary HTML, wait, what's that content tag…

1. Building The Template

HTML templates basically let you shove HTML/CSS/JS into a <template> tag for later re-use. A template tag is not rendered on page load (almost as if it had comments around it), instead it just sits there, waiting, until you to insert it into the document yourself.

Inside our <template> tag, we'll include a nifty <content> tag, which "acts as an insertion point for the shadowDOM" which basically means, that's where the content between the <doge-button> tags will go.

All we have to do to create our HTML Template^tm is put the doge-button HTML and CSS inside the <template> tag. Once the HTML & CSS is in the template, you'll notice the doge-button it doesn't render on the page anymore (as per the explanation above). To reference our template in JS, we'll give it the ID so-template.

<template id="so-template">
      <style>
        .doge-button{
            position: relative; 
            display: inline-block; 
            font-family: "Comic Sans MS", 'Comic Sans', "Marker Felt";
            cursor: pointer; 
        }

        .doge-face {
            background-image:   url('https://dl.dropboxusercontent.com/u/19492365/doge.png');
            height: 80px; 
            background-size: contain;
            background-repeat: no-repeat; 
            background-position: left center; 
        }

        .doge-button:hover .doge-txt{
            transform: rotate(-10deg); 
            color: yellow; 
            text-shadow: 2px 2px 0 magenta; 
        }

        .doge-txt{
            text-shadow: 0 0 0 #F0F;
            font-weight: bold;
            position: relative;
            margin-left: 30px;
            margin-bottom: 0px;
            margin-top: -30px;
            font-size: 18px;
            color: #F0F;
            transition: transform 0.1s ease-in, color 0.1s ease-in, text-shadow 0.1s ease-in;
            display: block;
        }
        .doge-button:hover:after{
            content: "WOW,"; 
            font-size: 18px;
            display: block; 
            position: absolute;
            top: 15px; 
            right: 0px; 
        }
      </style>

      <div class="doge-button">
          <div class="doge-face"></div> 
          <div class="doge-txt">
              <content></content>
          </div> 
      </div> 

</template>

We can now access our template in JS with a simple getElementById selector.

<script>
    var template = document.getElementById('soTemplate');
</script>

2. Creating The Custom Element

Custom Elements are simple, all you have to do make one is create a new object based on an HTMLElement prototype, insert a shadow root into the element during the elements createdCallback function, append a template to that shadow root, and register the element with the DOM. Bingo bango…

Simple right? Well, maybe not. The first thing you have to understand about this step is that every ordinary HTML element has a javascript 'interface', basically a javascript object corresponding to a tag. A tag like <input> corresponds to the HTMLInputElement object, which stores all the <input> elements behaviours and properties, such as the value property. These 'interface' objects all inherit from the HTMLElement object, which in turn inherits from the Element object. This is where that whole "Document Object Model" term comes from.

For our doge-button, we'll be inheriting from the basic HTMLElement prototype. By inheriting from HTMLElement, we'll bestow all the glorious functionality and behaviours of of a <div> tag onto our <doge-button>. I know… pretty exciting, but don't worry, we'll cover extending other elements later.

Now that we know what prototype we'll be inheriting from, we can create a new object based on that prototype.

<script>
    var template = document.getElementById('soTemplate');
    var muchPrototype = Object.create(HTMLElement.prototype);
</script>

3. Inserting The ShadowDOM Root

Once we have a prototype object, we attach a shadowDOM Root to it using a special createdCallback callback function. This callback is executed after the object is created (there are a few other custom element callbacks, but I'll let you play around with those later).

We create a new shadow root using the apty named createShadowRoot() function.

Once we have a shadow root, we append our template to it. We do that by cloning the template with document.importNode() (the 'true' parameter makes a 'deep clone'), and then we append that clone to the shadow root with appendChild().

<script>
    var template = document.getElementById('soTemplate');
    var muchPrototype = Object.create(HTMLElement.prototype);

    muchPrototype.createdCallback = function() {
      var root = this.createShadowRoot();
      var clone = document.importNode(template.content, true);
      root.appendChild(clone);  
    }
</script>

4. Registering the Custom Element

Now that we have our custom HTML Element, all we have to do to use it on the page is register it with the DOM. This is done with the document.registerElement function. The first parameter of this function is a string with the name of the new element. The custom element name must include a dash. Ours is 'doge-button'.

<script>
    var template = document.getElementById('soTemplate');
    var muchPrototype = Object.create(HTMLElement.prototype);

    muchPrototype.createdCallback = function() {
      var root = this.createShadowRoot();
      var clone = document.importNode(template.content, true);
      root.appendChild(clone);  
    }

    document.registerElement('doge-button', {
      prototype: muchPrototype
    });
</script>

5. Our <doge-button>

At this point, we can finally use our brand new <doge-button> element!

This is the entire HTML/CSS/JS for doing so:

<template id="soTemplate">
    <style>
        .doge-button{
            position: relative; 
            display: inline-block; 
            font-family: "Comic Sans MS", 'Comic Sans', "Marker Felt";
            cursor: pointer; 
        }

        .doge-face {
            background-image: url('https://dl.dropboxusercontent.com/u/19492365/doge.png');
            height: 80px; 
            background-size: contain;
            background-repeat: no-repeat; 
            background-position: left center; 
        }

        .doge-button:hover .doge-txt{
            transform: rotate(-10deg); 
            color: yellow; 
            text-shadow: 2px 2px 0 magenta; 
        }

        .doge-txt{
            text-shadow: 0 0 0 #F0F;
            font-weight: bold;
            position: relative;
            margin-left: 30px;
            margin-bottom: 0px;
            margin-top: -30px; 
            font-size: 18px;
            color: #F0F;
            transition: transform 0.1s ease-in, color 0.1s ease-in, text-shadow 0.1s ease-in;
            display: block;
        }

        .doge-button:hover:after{
            content: "WOW,"; 
            font-size: 18px;
            display: block; 
            position: absolute;
            top: 15px; 
            right: 0px; 
        }
    </style> 

    <div class="doge-button">
        <div class="doge-face"></div> 
        <div class="doge-txt">
            <content></content>
        </div> 
    </div> 
</template>

<script> 
    var template = document.getElementById('soTemplate');
    var muchPrototype = Object.create(HTMLElement.prototype);

    muchPrototype.createdCallback = function() {
      var root = this.createShadowRoot();
      var clone = document.importNode(template.content, true);
      root.appendChild(clone);  
    }

    document.registerElement('doge-button', {
      prototype: muchPrototype
    });
</script>

<doge-button>so custom… much element!</doge-button>

See the Pen GZKLNM by Jan Drewniak (@j4n) on CodePen.

Now, you can paste that into an HTML file (or codepen) and smoke it, or you can level up a bit and give your doge-button some extra buttony super powers.

6. Extending Existing Elements

At this point, out doge-button might look like a cool button, but it doesn't act like a button. We could potentially write a whole lot of javascript to make our doge-button act like a button, but why do that when we can just have it extend the HTMLButtonElement.

Extending a element requires making two changes to our code. First, instead of creating an object that inherits from HTMLElement, we make on that inherits from the HTMLButtonElement prototype.

var muchPrototype = Object.create(HTMLButtonElement.prototype);

Second, the process for registering an extended element with the DOM looks slightly different. We have to pass an additional extends property to the registerElement() function like this.

document.registerElement('so-button', {
  prototype: muchPrototype, 
  extends: 'button'
});

Once we do that, you'll notice the doge-button will look a bit different because it will also inherit all the default CSS styles of a <button> element. To remedy this ugliness, we can override the default button style using the :host CSS pseudo-selector. The following CSS should be enough to override the default button style.

    :host {
      background-color: transparent; 
      border: none;
    }

With these modifications our doge-button will actually function like a normal button. We use it on our page a little differently though, instead of using the <doge-button> tag, we now have to use our doge-button using the is attribute on a <button> element, like so.

<button is="so-button">much button</button>`

The final code for our super buttony doge-button looks like this:

<template id="soTemplate">
  <style>
    :host {
      background-color: transparent; 
      border: none;
    }
    .doge-button{
      position: relative; 
      display: inline-block; 
      font-family: "Comic Sans MS", 'Comic Sans', "Marker Felt";
      cursor: pointer; 
    }

    .doge-face {
      background-image:   url('https://dl.dropboxusercontent.com/u/19492365/doge.png');
      height: 80px; 
      background-size: contain;
      background-repeat: no-repeat; 
      background-position: left center; 
    }

    .doge-button:hover .doge-txt{
      transform: rotate(-10deg); 
      color: yellow; 
      text-shadow: 2px 2px 0 magenta; 
    }

    .doge-txt{
text-shadow: 0 0 0 #F0F;
  font-weight: bold;
  position: relative;
  margin-left: 30px;
  margin-bottom: 0px;
    margin-top: -30px; 
  font-size: 18px;
  color: #F0F;
  transition: transform 0.1s ease-in, color 0.1s ease-in, text-shadow 0.1s ease-in;
  display: block;
    }

    .doge-button:hover:after{
      content: "WOW,"; 
      font-size: 18px;
      display: block; 
      position: absolute;
      top: 15px; 
      right: 0px; 
    }
  </style> 
  <div class="doge-button">
  <div class="doge-face"></div> 
  <div class="doge-txt">
    <content></content>
  </div> 
</div> 
</template>

<button is="so-button" type='submit' alert-text="such button!">so click</button>

<button is="so-button" type='submit'>much button</button>

<script> 
var proto = Object.create(HTMLButtonElement.prototype);

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

proto.createdCallback = function() {
  var root = this.createShadowRoot();
  var clone = document.importNode(template.content, true);
  root.appendChild(clone); 
}

document.registerElement('so-button', {
  prototype: proto, 
  extends: 'button'
});
</script>

See the Pen VeejQj by Jan Drewniak (@j4n) on CodePen.

As you can see from this example, we've placed two doge-buttons on the page with different text. In the codepen embed, I've also added a event-listener on the custom button. See if you can spot how to do that ;).

6. HTML Import-ing Our Custom Element

You probably don't want to copy and paste the code above every time you want to use your <doge-button> right? Luckily, this is where HTML Imports come into play. HTML imports are pretty straight forward. They make it easy to include an external HTML file into your main HTML file. The Javascript in the imported file executed as if it were on the main file, so all the code we've written so far will still work when called from the imported file.

All we have to do to create an HTML is still all the code we've written so far into a separate HTML file, let's call it doge-button.html. We can then import it from any other HTML file with a simple <link> tag.

<head>
  <link rel="import" href="/path/to/doge-button.html">
</head>

Now we can use our doge-button in any HTML document we want!

Epilogue

Congratulations on making your first Web Component. Your probably basking in a feeling of achievement, but then all of a sudden, reality sinks in. The charts on caniuse.com look bleak, and the road to production usage of web components is still years away. Web Components are the future, and although you can't use this stuff in production now, you can at least take comfort in knowing that when the Web Components future does arrive, you'll be ready (unless of course the specs change or something better comes along).