Simplified Web Components via DOMClass

When I’ve talked in Helsinki about Custom Elements, I’ve said that probably one of the reason these were not so popular was due incompatibility with IE8, something I’ve eventually fixed, something that at the end of the day wasn’t a real game changer.

A group of 4 specs

There’s a lot of misunderstanding about Web Components: someone thinks they are Polymer, someone thinks they are just Custom Elements, the truth is these represent a huge specification divided in a group of 4: HTML Templates, Custom Elements, Shadow DOM, and HTML Imports.

Every sub-part of this spec is essential in order to have best performance and best usage of the entire Web Components “suite“ and we’re not even close to have cross platform adoption … but at least we are closer!

Shadow DOM v1 status Chrome: http://t.co/pXtwYxrXQj Saf: https://t.co/86Q3tq0Ble FF: https://t.co/nUmI5iuc8q Edge: http://t.co/WfOk1wpEWz

— Polymer (@polymer) September 16, 2015

Not so many alternatives

We’ve got Polymer, based on the same polyfills other Web Components libraries are based, Angular, which uses ng- attributes and directives in a “componentized fashion“, and React, which actually reinvented custom elements and components in its own way.

These are all great products, but also huge in terms of documentation and amount of details we need to know before we can start … but wouldn’t be cool if we could all start creating our custom elements, using some widely compatible technique to simulate a bit of Shadow DOM, and grouping all info in just one JS file so we can modularize them and import them as needed directly as standalone scripts?

Introduction to DOMClass

This dom-class library is just born, but it’s built on top of modules already used in production such es-class and, optionally when CSS is defined, restyle.

The idea of using es-class is to have today an empowered ES6/ES2015 Class like syntax without needing to transpile, simplifying in a semantic way common functionalities available via Custom Elements.

var XQuote = new DOMClass({
  // like anyn other class we have a constructor
  constructor: function (quote) {
    this.textContent = quote;
  }
});


// let's create our first component
document.body.appendChild(
  new XQuote('Powerful you have become')
);

Above example will create a name-conflict-free Custom Element, something that could only be initialized and used via JavaScript because unless we check the created nodeName property, we can’t style it or select it via query. If we’d like to be able to create via HTML such component, we can simply give it a name.

var XQuote = new DOMClass({
  name: 'x-quote',
  constructor: function (quote) {
    if (quote) this.textContent = quote;
  }
});

We have now the ability to create quotes via new XQuote('...') or just plain HTML:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <x-quote>Always pass on what you have learned</x-quote>
  </body>
</html>

So far, so good: we declare a class, we eventually specify the component name if we’d like to be able to style or reach it via CSS queries, but we also would like to be sure that this component comes with its own styles. How? Well … so far we’ve seen the es-class integration, now let’s have a look at restyle one.

var XQuote = new DOMClass({
  // the css property will create a style
  // dedicated to this component only
  css: {
    // to target this element
    // use its 'x-quote' name
    // or an empty '' string
    'x-quote': {
      display: 'block',
      backgroundColor: 'whitesmoke',
      borderLeft: '4px solid silver',
      padding: 8
    }
  },
  name: 'x-quote',
  constructor: function (quote) {
    if (quote) this.textContent = quote;
  }
});

Great! We can now be sure our quote will be shown as expected, and we still have the ability to style the element via regular CSS.

As it is for anything shared through a prototype, every single component will inherits the very same CSS. This is great from one point of view, but also capable of changing all elements at once if the instance would suddenly use its .css property to change its position, its opacity, or its background color.

The problem is automatically solved via DOMClass, which will create a dedicated .css property per each new x-quote component.

var XQuote = new DOMClass({
  css: {
    'x-quote': {
      display: 'block',
      backgroundColor: 'whitesmoke',
      borderLeft: '4px solid silver',
      padding: 8
    }
  },
  name: 'x-quote',
  constructor: function (quote) {
    if (quote) this.textContent = quote;
    this.css.overwrite({
      '': {
        backgroundColor: 'rgb(' + [
          Math.random() * 256 | 0,
          Math.random() * 256 | 0,
          Math.random() * 256 | 0
        ].join(',') + ')'
      }
    });
  }
});

We don’t even need to know how the single instance is going to be uniquely addressed once created, hence the reason the style overwrite uses an empty string '' to reference itself. Every single quote now will have a different background color, how cool is that?

Other Custom Elements goodies

We have a constructor, a style, we just need to react to common node states now: when the instance has been attached on the DOM (live), when it has been detached (offline), and when somebody changed its attribute.

Everything is there, as easy to remember and type as possible.

var XNotifier = new DOMClass({
  // optional, if specified will be used
  // as alias for createdCallback
  constructor: function () {
    console.log('constructor', arguments);
  },
  // optional, alias for attachedCallback
  onAttached: function () {
    console.log('attached');
  },
  // optional, alias for detachedCallback
  onDetached: function () {
    console.log('detached');
  },
  // optional, alias for attributeChangedCallback
  onChanged: function (name, prev, curr) {
    console.log('changed', arguments);
  }
});

// example
var xn = new XNotifier('hi', 'there');
document.body.appendChild(xn);
xn.setAttribute('test', 'me');
document.body.removeChild(xn);

Adding Mixins/Traits

Possibly the best es-class feature that is proposed for ES7/ES2016 but nowhere yet, we can use plain objects or universal mixins to compose our components functionality.

// Mixin Examples

// A JSON capable dataset based storage
var Data = {
  data: function data(key, value) {
    var v, k = 'data-' + String(key).replace(
      /([a-z])([A-Z])/g,
      function (m, l, U) {
        return l + '-' + U.toLowerCase();
      }
    ).toLowerCase();
    if (arguments.length === 2) {
      if (value == null) {
        this.removeAttribute(k);
      } else {
        this.setAttribute(k, JSON.stringify(value));
      }
    } else {
      v = this.getAttribute(k);
      return v == null ? v : JSON.parse(v);
    }
  }
};

// A shortcut for adding and removing events
var OnOff = {
  on: function on() {
    this.addEventListener.apply(this, arguments);
    return this;
  },
  off: function off() {
    this.removeEventListener.apply(this, arguments);
    return this;
  }
};

We can now compose any component as we like, bringing in one or more mixins through the magic with:

// dedicated to contain user card like info
var UserCard = new DOMClass({
  with: [Data, OnOff],
  name: 'user-card',
  css: {'': {display: 'block'}},
  constructor: function (info) {
    // either already on the dom or created via JS
    var data = info || this.data('info');
    this.textContent = info.name;
    this.data('info', data);
  }
});


// test example
var ucard = document.body.appendChild(
  new UserCard({
    name: 'Andrea',
    age: 37
  })
);
ucard.on('click', function (e) {
  console.log(this.data('info'));
});

Compatibility, Tests, and Dependencies

For this project I’ve abandoned IE8 support (mostly for a matter of time I don’t have) but I’ve kept IE9 and greater support. Firefox, Opera, Safari, and Chrome are also working on Desktop, while on Mobile Web every device from Android 2.3 to 6 would work, as well as Windows Phones, Blackberries, and others. Here the whole list.

There is also a live test page where you might eventually try directly in its console all snippets written in this post.

Last for this section, but not least, in case you’d like to be sure everything works as expected you might need some polyfill such dom4 and document-register-element.

Grabbing DOMClass All At Once: Vitamer JS

When I’ve posted about The Web Bare Necessities I’ve forgotten to mention I always use a bounce of polyfills and normalizer in order to make my life easier, and yet based on standards.

In this case I’ve added everything to the same mix in a file that, once minified and gzipped, weights less than 9KB.

This file is called vitamer.js, and ti comes with query-result flavor too via vitamer-qr.js.

Better explained in this section, vitamer brings in all the essential utilities and polyfills needed to make DOMClass work without worrying about external dependencies.

What about the is property?

I’m a strong believer in the is intent and strength, inheriting an HTMLInput without having one unless we create it as sub-node and delegate in there all focus, blur, and input events is a non-sense madness. However, the topic is still under discussion and I didn’t want to lock this project behind something not clear yet (as opposite to polymer that uses is as property to name everything).

Whenever the matter will be agreed and standardized, there’s room for improvements adding some extra magic so that simply specifying the following:

var MyInput = new DOMClass({
  extends: HTMLInput,
  name: 'my-input'
});

should do the trick, without going too fancy, and keeping it simple to use. We can eventually discuss the usage of is in the future anyway, right now let’s stick with what is certain and is already backward and forward compatible.

As Summary

As mentioned at the beginning, this is a very young project but while writing and testing it I’ve seen a lot of potentials. The standard based class approach without needing to reinvent the wheel over and over wins in terms of semantic, readability, and expressiveness, and the little magic brought via restyle gives every component the ability to really be standalone.

As it is for every other project of mine, the more it’s starred, the more chance we have to bring it to cdnJS … so, what are you waiting for? Go there and give me some love! :-)

Andrea Giammarchi

Fullstack Web Developer, Senior Software Engineer, Team Leader, Architect, Node.js, JavaScript, HTML5, IoT, Trainer, Publisher, Technical Editor