Web Components

in ES6 ES2015

Andreas Argelius

Nov 18, 2015

Andreas Argelius

Engineer originally from Sweden, recently moved to Japan to work with JavaScript development.

 

Enjoys studying Japanese, eating good food and drinking beer.

 

Avid runner currently preparing for Yokohama Marathon.

I help make a service called Monaca.

Monaca is ...

  • ... a building and packaging service for hybrid mobile apps.
  • ... a Cloud IDE optimized for hybrid app development.
  • ... a backend for hybrid apps.
  • ... a hybrid app debugger with live-reloading.

I also work on

A hybrid app UI component library

Onsen UI

Why is it called Onsen UI?

  • Onsen (温泉) is Japanese for "hot spring".
  • Very popular vacation spot. Both among people and monkeys.
  • Can also be translated to "spa".
  • SPA is used as an abbreviation of "Single Page Application".

Introducing

Onsen UI 2.0 Beta

  • Material Design
  • Custom Elements
  • Written in ES2015

Some of the features are:

AngularJS

React

Custom Elements

<h3>iOS switches</h3>
<ons-switch></ons-switch>
<ons-switch checked></ons-switch>
<ons-switch checked modifier="green"></ons-switch>

<h3>Material Design switches</h3>
<ons-switch modifier="material"></ons-switch>
<ons-switch modifier="material" checked></ons-switch>

All components are custom tags:

What is

Web Components?

 

  • Custom Elements
  • Shadow DOM
  • HTML templates
  • HTML imports

Actually Onsen UI is only using the Custom Elements API.

A collection of technologies including...

Creating

Custom Elements

Elements are defined using

document.registerElement(name, options)

It's a very simple API.

Example

  var proto = Object.create(HTMLElement.prototype);

  proto.createdCallback = function() {
    this.innerHTML = 'The Force will be with you.';
    
    setInterval (function () {
      this.innerHTML = this.innerHTML === 
        'Always.' ? 'The Force will be with you.' : 'Always.';
    }.bind(this), 2000);
  };

  document.registerElement('my-fancy-element', {prototype: proto});

Lifecycle callbacks

  • createdCallback()

  • attachedCallback()

  • detachedCallback()

  • attributeChangedCallback()

There are four different lifecycle callbacks:

Web Components in the wild

GitHub is using the Custom Elements API.

Browser support

From MDN:

Only supported in Chrome!

The polyfill the Huge!

  • The current version is 115KB minified.
  • The Shadow DOM polyfill requires 70KB.
  • We decided to use only use the Custom Elements API in Onsen UI. It's 16.4KB.

We have our reasons...

  • Too big. Far too big.
  • HTML imports doesn't fit into build pipeline.
  • Shadow DOM doesn't solve anything that proper namespacing does not.
  • JavaScript strings work fine as templates.

ES2015

The language formerly known as ES6

ES2015

  • New JavaScript standard finalized this year.
  • Currently not 100% supported in browsers.

New features

  • Classes. "class" has been a reserved word for a long time now. Finally it has some use.
  • Arrow functions. Another way to define functions.
  • Spread operator, Promises, etc.

ES6 classes

class Person {
  constructor(name = 'Andreas') {
    this.name = 'Andreas';
  }

  get name() {
    return this._name;
  }

  set name(n) {
    if (typeof n !== 'string') {
      throw new Error('Name must be a string.');
    }

    this._name = n;
  }

  greeting() {
    alert(`Hello! My name is ${this.name}!`);
  }
}

Introduces constructor, getters, setters, default arguments.

Arrow functions

let add = (a, b) => {
  return a + b;
};

// Even shorter
add = (a, b) => a + b;

// Nice in functional expressions.

let lengths = names.map((n) => n.length);

// Before
lengths = names.map(function(n) {
  return n.length;
});

Yet another way to define functions in JavaScript!

Binds lexical scope

function Person(age) {
  var that = this;

  that.age = age;

  setInterval (function () {
    that.age++;
  });
}
function Person(age) {
  this.age = age;

  setInterval (function () {
    this.age++;
  }.bind(this));
}
function Person(age) {
  this.age = age;

  setInterval (() => {
    this.age++;
  });
}

We used to do like this:

Or like this:

Arrow functions bind the lexical "this" value. So we can do like this:

ES2015 features not yet implemented in all browsers!

Babel

Babel will translate ES2015 code into code that can be interpreted and executed by all modern browers.

'use strict';

var fn = function fn(name) {
  console.log('May the force be with you, ' + name);
};

fn('Andreas');
const fn = (name) => {
  console.log(`May the force be with you, ${name}`);
};

fn('Andreas');
babel --presets es2015 force.js

Wouldn't it be great if we could make Custom Elements as class instances?

We can!

class HelloElement extends HTMLElement {
  createdElement() {
    this.innerHTML = `Hello, ${this.name}!`
  }

  attributeChangedCallback(name, from, to) {
    this.innerHTML = `Hello, ${to}`;
  }

  get name() {
    return this.getAttribute('name') || 'Andreas';
  }
}

document.registerElement('hello-there', HelloWorldElement);

Sample app

Source is available on GitHub

class StarRating extends HTMLElement {
  createdCallback() {
    const template = doc.querySelector('template') || document.createElement('template');
    const clone = document.importNode(template.content, true);

    this.createShadowRoot();
    this.shadowRoot.appendChild(clone);

    this._createStars().forEach((star) => {
      this.shadowRoot.appendChild(star);
    });

    this._colorStars();
  }

  attributeChangedCallback(name, from, to) {
    if (name === 'value') {
      this._colorStars();
    }
  }

  get value() {
    const v = parseFloat(this.getAttribute('value') || 0);
    if (!(v >= 0)) {
      return 0;
    }
    return v;
  }

  set value(v) {
    if (!(parseFloat(v) >= 0)) {
      throw new Error('Value must be a number larger or equal to 0.');
    }

    this.setAttribute('value', v);
  }

  get max() {
    return parseInt (this.getAttribute ('max') || 5);
  }

  set max(v) {
    throw new Error('The maximum value can\'t be changed.');
  }

  get _stars() {
    return Array.from(this.shadowRoot.childNodes)
      .filter ((node) => node.nodeName.toLowerCase () === 'span');
  }

  _createStars() {
    const stars = [];

    for (let i = 0; i < this.max; i++) {
      const star = document.createElement('span');
      star.innerHTML = '★';
      stars.push(star);

      const innerStar = document.createElement('span');
      innerStar.innerHTML = '★';
      star.appendChild(innerStar);
    }

    return stars;
  }

  _colorStars() {
    const value = this.value;
    let p;

    for (var i = 0; i < this.max; i++) {
      const star = this._stars[i];
      const innerStar = star.children[0];

      if (i + 1 <= value) {
        innerStar.style.width = '100%';
      }
      else {
        innerStar.style.width = 0;
      }

      p = value - i;

      if (p > 0 && p < 1) {
        innerStar.style.width = p * 100 + '%';
      }
    }
  }
}


window.StarRatingElement = document.registerElement('star-rating', StarRating);

Unit tests with Karma

Thank you for listening!

Web Components in ES2015

By Andreas A

Web Components in ES2015

  • 1,111