Is it Finally Time for Native Web Components?

https://slides.com/tylergraf/is-it-finally-time

Tyler Graf

Tech Lead - Tree Web

History

1998

HTML Components (HTC)

<PUBLIC:COMPONENT>
<PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="DoStuff()" />
<PUBLIC:ATTACH EVENT="onmouseout"  ONEVENT="Restore()" />
<SCRIPT LANGUAGE="JScript">
   var normalColor, normalSpacing;

   function DoStuff()
   {
     // save original values
     normalColor  = runtimeStyle.color;
     normalSpacing= runtimeStyle.letterSpacing;

     runtimeStyle.color  = "red";
     runtimeStyle.letterSpacing = 2;
   }

   function Restore()
   {
     // restore original values
     runtimeStyle.color  = normalColor;
     runtimeStyle.letterSpacing = normalSpacing;
   }
</SCRIPT>
</PUBLIC:COMPONENT>

1998

Open Sourced

2001

XML Binding Language (XBL)

<binding id="slideshow">
  <content>
    <xul:vbox flex="1">
      <xul:deck xbl:inherits="selectedIndex" selectedIndex="0" flex="1">
        <children/>
      </xul:deck>
      <xul:hbox>
        <xul:button xbl:inherits="label=previoustext"
                    oncommand="parentNode.parentNode.parentNode.page--;"/>
        <xul:description flex="1"/>
        <xul:button xbl:inherits="label=nexttext"
                    oncommand="parentNode.parentNode.parentNode.page++;"/>
      </xul:hbox>
    </xul:vbox>
  </content>

  <implementation>

    <constructor>
      var totalpages=this.childNodes.length;
      document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1]
              .setAttribute("value",(this.page+1)+" of "+totalpages);
    </constructor>

    <property name="page"
          onget="return parseInt(document.getAnonymousNodes(this)[0].childNodes[0].getAttribute('selectedIndex'));"
          onset="return this.setPage(val);"/>

    <method name="setPage">
      <parameter name="newidx"/>
      <body>
        <![CDATA[
          var thedeck=document.getAnonymousNodes(this)[0].childNodes[0];
          var totalpages=this.childNodes.length;  

          if (newidx<0) return 0;
          if (newidx>=totalpages) return totalpages;
          thedeck.setAttribute("selectedIndex",newidx);
          document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1]
                  .setAttribute("value",(newidx+1)+" of "+totalpages);
          return newidx;
        ]]>
      </body>
    </method>
  </implementation>

</binding>

2007

XBL2

2012

XBL2

2011

HTC

2010

2008

V0.0

any lib (jquery) - doesn't look like a component

their proposal - looks like a component

custom elements v1

Things they wrote to fill gaps

HTML & DOM

  • Custom Elements
  • <template>
  • Shadow DOM
  • HTML Imports
  • Model Driven Views
  • Mutation Observers
  • Subclassable DOM

JS

  • Classes

  • Traits & Interfaces

  • Traceur (transpiler)

  • Deferreds (Promises)

  • async/await

  • Object.observe()

  • TC39 "Stages" model

CSS

  • CSS Variables

  • CSS Mixins

  • Hierarchal CSS

  • CSS OM

  • Shadow Styling

  • Web Animations

  • Polyfills

TC39

Web Components Spec

  • HTML Templates

  • Custom Elements

  • Shadow DOM

  • HTML Imports

HTML Templates

<template id="party">
  <h1>Party Hard</h1>
  <img src="giphy.gif" alt="Party Hard">
</template>
let partyTemplate = document.createElement('template');
partyTemplate.innerHTML = `
  <h1>Party Hard</h1>
  <img src="giphy.gif" alt="Party Hard">
`;
let clone = document.importNode(partyTemplate.content,true);

document.body.appendChild(clone);

in HTML

in JS

Stamping

Custom Elements

class FSPerson extends HTMLElement {

  static get observedAttributes(){
    return ['name','gender'];
  }

  connectedCallback(){
    // called when added to DOM
  }

  attributeChangedCallback(name, oldValue, newValue){
    // called when an observed attribute changes
  }

  disconnectedCallback(){
    // called when removed from DOM
  }
}

window.customElements.define('fs-person', FSPerson);
<fs-person name="Tyler" gender="male"></fs-person>

Shadow DOM

const shadowRoot = header.attachShadow({mode: 'open'});

shadowRoot.innerHTML = `
  <h1>Hello Shadow DOM</h1>
`;

HTML Imports

<link rel="import" href="./fs-person.html">

All Together Now

<link rel="import" href="./fs-person.html">

<fs-person name="Tyler" gender="male"></fs-person>
<fs-person name="Lindsey" gender="female"></fs-person>
<template>
  <style>
    :host {
      display: block;
    }
    .gender {
      display: inline-block;
      height: 10px;
      width: 10px;
      background: #000;
    }
    .male {
      background: blue;
    }
    .female {
      background: pink;
    }
  </style>
  <span class="gender"></span>
  <span id="name"></span>
</template>

<script>
  let fsPersonTemplate = document.currentScript.ownerDocument.querySelector('template');

  class FSPerson extends HTMLElement {

    static get observedAttributes(){
      return ['name','gender'];
    }

    constructor(){
      super();
      this.attachShadow({mode: 'open'});

      let clone = document.importNode(fsPersonTemplate.content, true);
      this.shadowRoot.appendChild(clone);
    }
    attributeChangedCallback(attr, oldValue, newValue){
      if(attr === 'name'){
        this.shadowRoot.querySelector('#name').textContent = newValue;
      }
      if(attr === 'gender'){
        let gender = this.shadowRoot.querySelector('.gender');
        gender.classList.remove('female','male')
        gender.classList.add(newValue);
      }
    }
  }

  window.customElements.define('fs-person', FSPerson);


</script>

TC39

Web Components Spec

  • HTML Templates

  • Custom Elements

  • Shadow DOM

  • ES Modules

Oct 23

2015 Polymer 1.0

<link rel="import" href="../bower_components/polymer/polymer.html">

<dom-module id="fs-person">
  <template>
    <style>
      :host {
        display: block;
      }
      .gender {
        display: inline-block;
        height: 10px;
        width: 10px;
        background: #000;
      }
      .male {
        background: blue;
      }
      .female {
        background: pink;
      }
    </style>
    <span class$="gender [[gender]]"></span>
    <span id="name">[[name]]</span>
  </template>
  <script>
  (function () {
    Polymer({
      is: 'fs-person',
      properties: {
        name: {
          type: String
        },
        gender: {
          type: String,
          value: 'male'
        }
      },
      attached: function () {
        //called when added to DOM
      },
      detached: function() {
        //called when removed from DOM
      }
    });
  })();
  </script>
</dom-module>

Template Helpers

<template is="dom-if" if="[[hasGender]]">
  <span>I'm a [[gender]]</span>
</template>
<template is="dom-repeat" items="[[people]]">
  <fs-person name="[[item.name]]"></fs-person>
</template>

Repeat

if

<h3 data-test$="person-header">People</h3>
<fs-person name="Sam"></fs-person>

properties and attributes

<input value="{{nameValue::input}}">

double data bindings

Property Effects

Polymer({
  is: 'fs-person',
  properties: {
    name: {
      type: String,
      notify: true
    },
    gender: {
      type: String,
      reflectToAttrubute: true,
      value: 'male'
    }
  }
});
Polymer({
  observers: [
    '_nameObserver(name, gender)'
  ],
  _nameObserver: function(name, gender){
    // do stuff when both name and gender change
  }
});

Observers

Keeps properties and attributes in sync

class FSPerson extends HTMLElement {
  static get observedAttributes(){return ['name','gender']}  
  attributeChangedCallback(name, old, val){
    if(name){
      this._name = val;
      this._changeNameInTemplate(val);
    }
  }
}

Native

Polymer 1

  • First attempt at framework for web components
  • Built on web components v0 spec
  • Couldn't use classes
  • Worked out polyfills
  • 40KB

2017 Polymer 2.0

<link rel="import" href="../bower_components/polymer/polymer-element.html">

<dom-module id="fs-person">
  <template>
    <style>
      :host {
        display: block;
      }
      .gender {
        display: inline-block;
        height: 10px;
        width: 10px;
        background: #000;
      }
      .male {
        background: blue;
      }
      .female {
        background: pink;
      }
    </style>
    <span class$="gender [[gender]]"></span>
    <span id="name">[[name]]</span>
  </template>
  <script>

    class FSPerson extends Polymer.Element {
      static get is() {return 'fs-person'}
      static get properties() {
        return {
          name: {
            type: String
          },
          gender: {
            type: String,
            value: 'male'
          }
        }
      }
      connectedCallback(){
        // called when added from DOM
      }
      disconnectedCallback(){
        // called when removed from DOM
      }
    };

    window.customElements.define('fs-person', FSPerson);
  </script>
</dom-module>

Polymer 2

  • Classes!!
  • custom elements v1
  • shadow dom v1
  • removed a lot of api
  • 12KB

2018 Polymer 3.0

import {PolymerElement, html} from '../node_modules/@polymer/polymer/polymer-element.js'

class FSPerson extends PolymerElement {
  static get template() {
    return html`
      <style>
        :host {
          display: block;
        }
        .gender {
          display: inline-block;
          height: 10px;
          width: 10px;
          background: #000;
        }
        .male {
          background: blue;
        }
        .female {
          background: pink;
        }
      </style>
      <span class$="gender [[gender]]"></span>
      <span id="name">[[name]]</span>
    `;
  }

  static get is() {return 'fs-person'}
  static get properties() {
    return {
      name: {
        type: String
      },
      gender: {
        type: String,
        value: 'male'
      }
    }
  }
  connectedCallback(){
    // called when added from DOM
  }
  disconnectedCallback(){
    // called when removed from DOM
  }
}

window.customElements.define('fs-person', FSPerson);

Polymer 3

  • bower => npm
  • html imports => es modules
  • polymer 3 is deprecated

lit-html

  • No observers
  • Change to templating
  • 3.5KB (lit-html)
  • 6KB (lit-element)

Lit

fs-person

import {LitElement, html} from '../node_modules/@polymer/lit-element/lit-element.js'

class FSPerson extends LitElement {
  render() {
    return html`
      <style>
        :host {
          display: block;
        }
        .gender {
          display: inline-block;
          height: 10px;
          width: 10px;
          background: #000;
        }
        .male {
          background: blue;
        }
        .female {
          background: pink;
        }
      </style>
      <span class="gender ${this.gender}"></span>
      <span id="name">${this.name}</span>
    `;
  }

  static get properties() {
    return {
      name: {
        type: String
      },
      gender: {
        type: String
      }
    }
  }
}

window.customElements.define('fs-person', FSPerson);

Compare

  • if
  • repeat
  • until
  • observers

Is it time for Native Web Components?

Yes, if component is simple enough

Yes, with a template engine if more complex

Required Watching

Go and Do

Made with Slides.com