My latest Flickr photograph

Diving deeper into custom elements in mozilla

Sunday 24th of March 2013, 04:38:34 pm

Let's talk about web components some more, shall we?

In the last post I talked about decorators. Those things are great, but also fictional. The CSS toggling turns out to be such a headache to do right, safely, that you need more than one head to suffer it. However, custom elements are exactly as functional, so let's look at those. Turns out they do exactly the same thing, just differently. First off, the spec, which can be found uponyonderhyperlink:

https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html

It lets us define our own elements, with all the associated browser behaviour. If we didn't have <audio> or <video>, this would let us add it ourselves without having to worry about browser support. So let's have a look at an example:

  <element name="my-element">
    <template></template>
  </element>

This is the minimal custom element: an element needs a name -containing a hyphen-, and a template child. It may also have any number of script elements, which is where the true magic will happen. In fact, the above example is a bit silly, let's just jump straight to a real custom element:

  <element name="webmaker-media" attributes="start end">
    <script>
      // This script runs once, when we load/import the component
      if(!window.Popcorn) {
        var script = document.createElement("script");
        script.src = "https://popcorn.webmaker.org/src/butter.js";
        document.head.appendChild(script); }
    </script>
    <template>
      <style scoped>
        webmaker-media {
          /* desired styling goes here */
        }
      </style>
      <content></content>
    </template>
    <script>
      // this also runs only once
      var template = this.querySelector("template");
      var templateContent = template.innerHTML;
      this.register({
        prototype: {
          // But this one is crucial: it gets called every
          // time we create this element, either through JS
          // or as a normal HTML element in the DOM.
          readyCallback: function() {
            var e = this;
            var h = this.innerHTML;
            var c = templateContent.replace("<content></content>", h);
            this.innerHTML = c;
            var start = this.getNamedItem("start");
            var end = this.getNamedItem("end");
            var div = this.querySelector("div");
            div.setAttribute("start",start);
            div.setAttribute("end",end);
          }
        }
      });
    </script>
  </element>

Okay, slight wall of HTML, but let's analyse that thing, because it's going to turn the HTML world upside down. Or rather, it'll complement it.

  <element name="webmaker-media" attributes="start end">
    [...]
  </element>

This defines a new element with tag name "webmaker-media", and it says it can take two attributes, "start" and "end". No "data-start", just "start". Super convenient.

  <element name="webmaker-media" attributes="start end">
    <script>
      [...]
    </script>
    [...]
  </element>

Any script tied to a custom element executes when the element definition is read in. This means that if we need special functionality for a custom element, we're free to import and load anything and everything that is necessary for this element to do its job on a page. We're allowed zero or more script elements, so we can nicely separate by function and namespace.

  <element name="webmaker-media" attributes="start end">
    [...]
    <template>
      <style scoped>
        webmaker-media {
          /* desired styling goes here */
        }
      </style>
      <p><content></content></p>
    </template>
    [...]
  </element>

The template is a special element that can contain HTML code relevant to your new element. At the least, this usually involves a style element with the incredibly important keyword "scoped". This keyword ensures that the styling will only apply to "this element or its descendants", so you can't pollute the global styling. Then there is the "content" element, which serves as a convenient element that can be replaced with the content found when the custom element is actually used. For instance, if in our normal HTML document we use this:

  <webmaker-media start="0" end="24">Monkeys monkey monkey.</webmaker-media>

then the template replacement would look and act like:

  <webmaker-media start="0" end="24">
    <style scoped>
      webmaker-media { /* ... */ }
    </style>
    <p>Monkeys monkey monkey.</p>
  </webmaker-media>

At this point we have all the "HTML" bits sorted, but a custom element that doesn't do anything special isn't very useful. Let's give our custom element some superpowers:

  <element name="webmaker-media" attributes="start end">
    [...]
    <script>
      var template = this.querySelector("template");
      var templateContent = template.innerHTML;
      this.register({
        prototype: {
          readyCallback: function() {
            // ...
          }
        }
      });
    </script>
  </element>

Inside any <element> script block, "this" refers to the <element> DOM object. That means it can querySelect subfragments, check parentNode, access the .children NodeList, and other such useful things. However, it also has one very special function: .register()

The register function lets us tack things onto the element's prototype, most important of which is the readyCallback function, which is triggered everytime a custom element of our new type is created. This function is the place where, for instance, we tack "default" event handling onto our element, so that if we're -for instance- creating a new kind of super-movable-button element, we can assign it a default click event, because it's a button, and a default click-drag event, because we claimed it was movable, and be sure that every instance of our draggable button responds to clicks and click-drags. (note: the current spec puts this functionality under a lifecycle interface, calling it the ".created()" function, so what it ends up actually being called is still up for grabs; the important part is what it does when it is called, rather than what it is called, of course).

In our own example element, above, we had this readyCallback function:

  readyCallback: function() {
    var e = this;
    var h = this.innerHTML;
    var c = templateContent.replace("<content></content>", h);
    this.innerHTML = c;
    var start = this.getNamedItem("start");
    var end = this.getNamedItem("end") || 0;
    console.log("start is " start " and end is " end);
  }

This first creates a local reference to "this" (in case we're going to set up anonymous functions; any properly complex custom element will have a fair number of anonymous functions, so: good practice). It then performs templating; while the spec is still being worked on, we have to do this ourselves, but the idea is that this ends up being a "comes supplied right out of the box" part of custom elements. Still, no biggy, it's easy enough to do because we have access to our template element.

After that, we grab our attribute values; we declared our element as having valid attributes "start" and "end", so we can grab those using the standard DOM ".getNamedItem(...)" function. We don't have to do any silly "data-..." attribute accessing, it's just a normal attribute. We then log the values to the console, because we can, but in a real element we'd use these values to set up something functionally sensible.

So what's the take-home message? "If browsers didn't have anything except a base HTML element, a JS engine, a CSS processor, and knowledge of how to create Custom Elements, we could implement all of HTML5 in it. Including ALL the events handling, default behaviour, src fetching, media playing, and so on and so forth". This thing allows you to make EVERYTHING you need to do what you want, encapsulated in neat, tidy, <element> packages that wrap all the HTML, CSS and JS required to make things work.

Did I mention you can put this in its own file? I may not have. You can put this in its own file, and then link to that file using a normal <link> element; Effectively, if I were to put all the "webmaker" elements in its own file and hosted it on "http://components.webmaker.org/all-elements.html", you could use them, and all you had to do would be to include this one line in your own source:

  <link rel="component" href="http://components.webmaker.org/all-elements.html">

Done. Without having to do anything else, you can now use a <webmaker-media> element anywhere on your page, and it'll do for you exactly the same thing that it does for me. It is, effectively, a new common HTML element that we, or more specifically: EVERYONE can use.

This changes everything.