Web Components Frameworks Overview

Alexander Tchitchigin

Positive Technologies

Web Components

  • Custom Elements
  • Shadow DOM
  • HTML Templates
  • ES6 Modules

Custom Elements

class FlagIcon extends HTMLElement {
  constructor() {
    super();
    this._countryCode = null;
  }

  static get observedAttributes() { return ["country"]; }

  attributeChangedCallback(name, oldValue, newValue) {
    // name will always be "country" due to observedAttributes
    this._countryCode = newValue;
    this._updateRendering();
  }
  connectedCallback() {
    this._updateRendering();
  }

  get country() {
    return this._countryCode;
  }
  set country(v) {
    this.setAttribute("country", v);
  }

  _updateRendering() {
    // Perform the rendering using Shadow DOM and/or HTML Templates
  }
}

customElements.define("flag-icon", FlagIcon);

Shadow DOM

class MyElement extends HTMLElement {  
  static get observedAttributes() {    
    return ['disabled'];  
  }

  constructor() {    
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `      
      <style>        
        .disabled {          
          opacity: 0.4;        
        }      
      </style>      

      <div id="container"></div>    
    `;

    this.container = this.shadowRoot('#container');  
  }

  attributeChangedCallback(attr, oldVal, newVal) {    
    if(attr === 'disabled') {      
      if(this.disabled) {        
        this.container.classList.add('disabled');      
      }      
      else {        
        this.container.classList.remove('disabled')      
      }    
    }
  }
}

HTML Templates

class MyElement extends HTMLElement {
  ...

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

    this.shadowRoot.innerHTML = `
      <template id="view1">
        <p>This is view 1</p>
      </template>

      <template id="view1">
        <p>This is view 1</p>
      </template>

      <div id="container">
        <p>This is the container</p>
      </div>
    `;
  }

  connectedCallback() {
    const content = this.shadowRoot.querySelector('#view1').content.clondeNode(true);
    this.container = this.shadowRoot.querySelector('#container');
    
    this.container.appendChild(content);
  }
}

Frameworks

Polymer

  • The Elder One
  • From Google
  • Completely rewritten
  • Tools and Libraries
  • TypeScript and Redux

Polymer

export class HnUserElement extends connect(store)(LitElement) {
  static get styles() {
    return [
      sharedStyles,
      css`
      table {
        margin: 1em 0;
      }`
    ];
  }

  render() {
    const user = this._user || {};
    return html`
    <hn-loading-button .loading="${user.isFetching}" @click="${this._reload}">
    </hn-loading-button>
    <table ?hidden="${user.failure}">
      <tr>
        <td>User:</td>
        <td>${user.id}</td>
      </tr>
      <tr>
        <td>Created:</td>
        <td>${user.created}</td>
      </tr>
      <tr>
        <td>Karma:</td>
        <td>${user.karma}</td>
      </tr>
    </table>
    ${user.failure ? html`<p>User not found</p>` : null}`;
  }

  @property()
  private _user?: UserState;

  _reload() {
    store.dispatch(fetchUser(this._user));
  }

  stateChanged(state: RootState) {
    const user = currentUserSelector(state);
    if (user) {
      updateMetadata({ title: user.id });
      this._user = user;
    }
  }
}

customElements.define('hn-user', HnUserElement);

Polymer

LitElement

  • Powers Polymer
  • From Google
  • Light(er) weight
  • TypeScript and all
  • Embeddable
  • No state management

LitElement

import {LitElement, html, css, customElement, property} from 'lit-element';

@customElement('my-element')
export class MyElement extends LitElement {

  // This decorator creates a property accessor that triggers rendering and
  // an observed attribute.
  @property()
  mood = 'great';

  static styles = css`
    span {
      color: green;
    }`;

  // Render element DOM by returning a `lit-html` template.
  render() {
    return html`Web Components are <span>${this.mood}</span>!`;
  }

}

Stencil

  • "A toolchain for building Design Systems"
  • From Ionic Framework
  • One-way data binding
  • TypeScript, router, redux
  • Uses JSX

Stencil

import { Component, Prop } from '@stencil/core';

@Component({
  tag: 'my-first-component',
})
export class MyComponent {

  // Indicate that name should be a public property on the component
  @Prop() name: string;

  render() {
    return (
      <p>
        My name is {this.name}
      </p>
    );
  }
}

Skate.js

  • Functional reactive abstraction over view technology
  • Supports React, Preact, lit-html
  • No state management
  • Embeddable

Skate.js

import Element, { html } from '@skatejs/element-lit-html';

export default class extends Element {
  static props = {
    name: String
  };
  render() {
    return html`
      Hello, ${this.name}!
    `;
  }
}

Slim.js

  • Not a framework :)
  • Lightweight, embeddable
  • One-way data bindings
  • Templates

Slim.js

import { tag, template, useShadow } from "slim-js/Decorators";
import { Slim } from "slim-js";

@tag('todo-item')
@template(/*html*/`
<style>
  ${require('./todo-item.css')}
</style>
  <li bind:class="getClass(data.checked)">
    <input s:id="checkbox"
      bind.boolean:checked="data.checked"
      type="checkbox" change="onChecked" />
    <label>{{data.text}}</label>
    <button click="onRemove">x</button>
  </li>
`)
@useShadow(true)
export default class TodoItem extends Slim {
  onChecked () {
    this.checked = this.data.checked = this.checkbox.checked
    this.commit()
  }

  getClass (checked) {
    return checked ? 'completed' : ''
  }

  onRemove (e) {
    this.callAttribute('on-remove', this.data)
  }

}

References

Thank you!

Questions?

Web Components Frameworks Overview

By Alexander Letov

Web Components Frameworks Overview

A brief overview of Web Components and several frameworks built on top of them.

  • 81