12 mei 2021 #Frontend

Door Wessel Loth access_time20 min

Web Components 101 – Part 2: Libraries

Web components. Een term die steeds vaker terugkomt. Maar net als veel andere tech-hypes lijkt de belofte van web components nog niet waar gemaakt te zijn. Of toch wel? Web components zijn al lang niet meer zo obscuur als een paar jaar geleden. Alleen in Nederland al gebruiken een aantal grote organisaties ze al meerdere jaren in productie. Steeds meer teams herkennen de meerwaarde van de onafhankelijkheid die webstandaarden met zich meebrengen.

Echter vind ik dat ze nog niet altijd in even goed daglicht staan! Daarom neem ik je de komende weken mee in het gedachtegoed, de basics, het ecosysteem, en laat ik zien hoe je een volwaardige frontend applicatie kan bouwen met web components.

Dit is het tweede deel van deze blogpost serie over Web Components. Heb je deel een nog niet gelezen? Klik hier om hem alsnog te bekijken!

 

Web Components 101 – Part 2: Libraries

Wat heeft een library je te bieden?

Zoals je misschien hebt gezien in deel één van deze blogpost serie over Web Components is het vrij makkelijk om een Custom Element te maken. Tóch is er nog aardig wat handwerk nodig om bijvoorbeeld de Shadow Root aan te maken, en om een HTML Template te clonen. In een kleine codebase is dat niet zo’n probleem, maar in een grote applicatie zorgt dit al snel voor veel code duplicatie.

Waarschijnlijk ga je dan vanzelf al herbruikbare stukjes code maken; helpers voor interactie met de Shadow DOM, utilities om efficiënt HTML Templates te manipuleren. Maar waarom opnieuw het wiel uitvinden? Om op een fijne manier te werken met web components, zijn er verschillende libraries die uitbreiden op de standaarden. Ze brengen veelal sugar syntax, handige utilities en extra manieren om efficiënt DOM te maken en te wijzigen. Libraries zijn uiteraard niet nodig om te werken met web components, maar ik laat je graag zien wat de voordelen zijn ten opzichte van een standaard Custom Element.

In deze blogpost ga ik een aantal verschillende libraries vergelijken met een native implementatie. Met elke library heb ik dezelfde feature gebouwd: een counter, met een plus en min knop. Wanneer de knoppen worden ingedrukt moet het getal worden aangepast, én moet het nieuwe getal op de pagina getoond worden. Deze feature laat de belangrijkste basics zien: data binding, state, de (rendering) lifecycle, styling en de algemene verschillen in syntax.

 

Native

We duiken gewoon direct de code in! Om een goede vergelijkingsbasis te hebben, laten we eerst zien hoe je deze feature kan bouwen zonder afhankelijkheden: een native Custom Element.

const template = document.createElement('template');
template.innerHTML = `
  <style>
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 4rem;
      height: 4rem;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  </style>
  <button id="dec">-</button>
  <span id="count"></span>
  <button id="inc">+</button>`;

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0; // Default count
    this.attachShadow({ mode: 'open' }); // Create a shadow tree
  }

  connectedCallback() {
    // When the component becomes active on the page, fill the shadow tree
    // with the content from the HTML template, and attach event listeners.
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this.shadowRoot.getElementById('inc').onclick = () => this.inc();
    this.shadowRoot.getElementById('dec').onclick = () => this.dec();
    this.shadowRoot.getElementById('count').innerHTML = 0;
  }

  inc() {
    this.shadowRoot.getElementById('count').innerHTML = ++this.count;
  }

  dec() {
    this.shadowRoot.getElementById('count').innerHTML = --this.count;
  }
}

customElements.define('my-counter', MyCounter);

Wat opvalt in dit voorbeeld is dat er redelijk wat boilerplate nodig is om een template met JavaScript te definiëren en daarna toe te voegen aan de shadow DOM. Daarnaast moet de querySelector (via getElementById) gebruikt worden om harde referenties naar de knoppen te leggen.

Net als met “ouderwets” JavaScript programmeren kan je met onclick of attachEventListener luisteren naar click events. Ook moet de innerHTML property direct worden aangesproken om de inhoud van de counter te wijzigen. Wat wel makkelijk werkt is dat direct na het updaten van de innerHTML property het nummer in de pagina ook daadwerkelijk wijzigt. Daar zijn geen extra lifecycle triggers of andere events voor nodig.

Nu de baseline is gezet, kunnen we verschillende libraries gaan vergelijken met deze native implementatie!

 

Lit

  • NPM Package naam: lit (voorheen lit-element)
  • Maintainer: Google (Polymer Project)
  • Downloads per week: ±280.000
  • Wie het gebruikt: Microsoft, Adobe, Google, GitHub, ING Bank
  • Website: https://lit.dev/

We beginnen met de meest bekende en populaire library binnen het web components ecosysteem: Lit, tot voor kort bekend als LitElement. Lit is een kleine library die bestaat uit een paar onderdelen: een klasse om jouw component van over te erven, verschillende tools om dynamisch HTML op te bouwen wat extra utilities en sugar syntax functies om alles nét wat makkelijker te maken.

Lit is een library gebouwd door developers van het Polymer Project, een onderdeel van Google dat zich bezig houdt met het maken van libraries, tools en standaarden om te werken met web components. Het maakt gebruik van lit-html, een kleine rendering library om supersnel HTML te kunnen aanmaken en aanpassen. Onder de motorkap maakt lit-html gebruik van HTML templates.

Lit is vrij kaal op zichzelf; een bewust keuze. Door focus te leggen op de belangrijkste functionaliteit, kunnen developers zelf keuzes maken in hoe de rest van de toolchain er uit komt te zien.

import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-counter')
export class MyCounter extends LitElement {
  @property() public count = 0;

  static styles = css`
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 4rem;
      height: 4rem;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  `;

  inc() {
    this.count++;
  }

  dec() {
    this.count--;
  }

  render() {
    return html`
      <button @click=${this.dec}>-</button>
      <span>${this.count}</span>
      <button @click=${this.inc}>+</button>
    `;
  }
}

Dit is hetzelfde counter component gebouwd in Lit. De code is geschreven met TypeScript, omdat er in Lit hele handige decorators zitten ingebouwd om op een bondige manier bijvoorbeeld properties te definiëren. Je kan echter ook prima Lit-componenten bouwen in JavaScript, het zijn simpelweg een paar extra regels code die je zal moeten onderhouden.

De syntax van een Lit component doet erg denken aan class-based React. Er is een klassedefinitie met properties, verschillende methodes en aan het einde een render functie die HTML teruggeeft. De HTML code wordt door middel van de html-tag gerenderd in de pagina.

Veel van de details van werken met web components zijn hier uit handen genomen, bijvoorbeeld het aanmaken van een HTML template of een Shadow Root. Dit wordt door Lit allemaal onder de motorkap geregeld, zodat jij je kan bezig houden met het bouwen van features in plaats van het handmatig managen van de Shadow DOM.

 

FAST   

  • NPM Package naam: @microsoft/fast-element
  • Maintainer: Microsoft
  • Downloads per week: ±1700
  • Wie het gebruikt: Microsoft
  • Website: https://www.fast.design/

​​

FAST is een verzameling van componenten en utilities om te werken met web components. Het bestaat uit een aantal lagen van abstractie, zodat je precies kan kiezen hoe jij wilt werken met web components. Om het gemakkelijk te maken, heb ik hier even een kort overzicht gemaakt van de onderdelen en hun doel.

@microsoft/fast-components (Design System)

Complete set van componenten, met een moderne look. Goed uitbreidbaar met thema’s om het zoveel mogelijk aan te laten sluiten bij jouw wensen.

@fluentui/web-components (Design System)

Complete set van herbruikbare componenten, met Microsoft Fluent UI styling, zoals in Office applicaties als Microsoft Teams.

@microsoft/fast-foundation (Whitelabel Components)

Complete set van herbruikbare componenten, zonder styling, om eenvoudig een maatwerk thema op aan te sluiten. De twee genoemde design systems zijn implementaties van @microsoft/fast-foundation, met een eigen thema.

@microsoft/fast-element (Web Component Library)

De fundatie van het FAST ecosysteem. Met FAST Element kan je zelf web components bouwen met behulp van sugar syntax en verschillende utilities.

Voor dit onderdeel van de vergelijking focussen we op @microsoft/fast-element, omdat dit de meeste controle geeft over de implementatie. Mocht je echter dus op zoek zijn naar een complete design system, dan kan FAST een goede keuze zijn.

import { html, css, customElement, observable, FASTElement } from '@microsoft/fast-element';

const template = html<MyCounter>`
  <button @click="${(x) => x.changeCount(x.count - 1)}">-</button>
  <span>${(x) => x.count}</span>
  <button @click="${(x) => x.changeCount(x.count + 1)}">+</button>
`;

const styles = css`
  * {
    font-size: 200%;
  }

  span {
    width: 4rem;
    display: inline-block;
    text-align: center;
  }

  button {
    width: 64px;
    height: 64px;
    border: none;
    border-radius: 10px;
    background-color: seagreen;
    color: white;
  }
`;

@customElement({
  name: 'my-counter',
  template,
  styles,
})
export class MyCounter extends FASTElement {
  @observable count: number = 0;

  changeCount(newCount: number) {
    this.count = newCount;
  }
}

De syntax voor een component gebouwd met FAST doet sterk denken aan componenten binnen het Angular framework. Zo wordt er veel aan elkaar geknoopt in de decorator van de klasse, waardoor de code binnen de klassedefinitie kan worden gereduceerd tot een duidelijk leesbare samenvatting. Daardoor zou je heel makkelijk de template en styles in een los bestand kunnen zetten om code wat duidelijker te scheiden, en ook styles te hergebruiken over meerdere componenten heen.

Wat opvalt is dat wanneer je iets wilt tonen in HTML, dat er een arrow-functie moet worden geschreven waar de eerste parameter verwijst naar de instantie van de klasse. Dit kan erg herhalend worden, waar je in sommige componenten wel tientallen keren dezelfde arrow-functie moet herhalen om een lijstje met properties te renderen.

 

Stencil

  • NPM Package naam: @stencil/core
  • Maintainer: Ionic
  • Downloads per week: ±120.000
  • Wie het gebruikt: Rabobank
  • Website: https://stenciljs.com/

Stencil is een compleet pakket om zoveel mogelijk configuratie en boilerplate uit handen te nemen van developers. Het buildsysteem is totaal verborgen en beheerd door Stencil, maar makkelijk uitbreidbaar door middel van Rollup plugins wanneer nodig. Stencil heeft gedetailleerde documentatie beschikbaar voor hoe je Stencil web components kan integreren met Angular, React, Vue en meer.

De CLI tooling is erg compleet. Net als met de Angular CLI, kan de Stencil CLI tooling componenten genereren, unit tests uitvoeren en de development server opstarten. Ook biedt Stencil standaard Server Side Rendering via Hydration aan, wat nog vrij onbekend is binnen het web component development ecosysteem. Mocht SEO dus een zwaarwegende beslissingsfactor zijn in jouw keuzeproces, dan is Stencil zeker het overwegen waard.

import { h, Component, State, Host } from '@stencil/core';

@Component({
  tag: 'my-counter',
  styles: `
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 4rem;
      height: 4rem;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  `,
  shadow: true,
})
export class MyCounter {
  @State() count: number = 0;

  inc() {
    this.count++;
  }

  dec() {
    this.count--;
  }

  render() {
    return (
      <Host>
        <button onClick={this.dec.bind(this)}>-</button>
        <span>{this.count}</span>
        <button onClick={this.inc.bind(this)}>+</button>
      </Host>
    );
  }
}

Stencil syntax doet net als FAST Element ook sterk denken aan het Angular framework, met het gebruik van een decorator voor de klassedefinitie. Echter wordt er ook gebruik gemaakt van een render functie, die bij Stencil standaard gebruik maakt van TSX syntax om op een nette manier HTML-in-JS te schrijven.

In dit voorbeeld worden de geavanceerde mogelijkheden van Stencil niet getoond. Zo is er bijvoorbeeld een duidelijke scheiding tussen private en public functionaliteit voor een glasheldere integratie met andere componenten. Dit en nog veel meer mogelijkheden maken Stencil tot een compleet pakket, wat robuust en volwassen aanvoelt.

 

Angular Elements

Het Angular framework heeft ook een mogelijkheid om componenten te bouwen als web components in plaats van Angular components. Dit heeft een groot voordeel: je kan Angular components maken zoals je gewend bent, met het gemak van het verpakken als een web component om het component overal te gebruiken.

import { Input, Component, ViewEncapsulation, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'my-counter',
  template: `
    <button (click)="dec()">-</button>
    <span>{{ count }}</span>
    <button (click)="inc()">+</button>
  `,
  styles: [
    `
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    `,
  ],
  encapsulation: ViewEncapsulation.ShadowDom,
})
export default class MyCounter {
  @Input() count: number = 5;

  constructor(private cd: ChangeDetectorRef) {}

  dec() {
    this.count--;
    this.cd.detectChanges();
  }

  inc() {
    this.count++;
    this.cd.detectChanges();
  }
}

Zoals je ziet lijkt dit enorm veel op een standaard Angular component. De enige twee uitzonderingen zijn dat ViewEncapsulation.ShadowDom wordt gebruikt als encapsulation setting, en dat de Change Detection handmatig wordt aangestuurd. Dit laatste heeft te maken met zone.js: de library die Angular onder water gebruikt om te identificeren wanneer er wijzigingen plaatsvinden die een re-render moeten triggeren. Het nadeel van deze library is dat die enorm groot is. Dit kan je in Angular gemakkelijk uitzetten, maar dan moet je wel handmatig met de ChangeDetectorRef wijzigingen aangeven.

Echter zijn er ook nadelen. Het is lastig om goede documentatie te vinden van hoe je Angular Elements in verschillende scenario’s kan gebruiken. Ook is de grootte van de bundle een stuk groter dan een native web component: een groot deel van het Angular framework moet worden ingeladen om een enkele web component op een pagina te kunnen gebruiken. Zo kan een simpele herbruikbare knop of andere kleine feature al snel tientallen Kilobytes groot zijn.

 

 

De bovenstaande libraries zijn natuurlijk niet de enige die het makkelijker maken om met web components te werken. Misschien wel de bekendste die ik nog niet heb genoemd is Polymer.

Net als Lit is Polymer onderdeel van het Polymer Project van Google. Met redelijk veel bekendheid en veel documentatie beschikbaar is het makkelijk om met Polymer te beginnen. Echter wordt het gebruik van Polymer niet meer aangeraden door het Polymer development team zelf. In plaats daarvan wordt het gebruik van Lit aangeraden, vanwege de kleinere library grootte en de moderne manier van werken.

Andere libraries die je wel eens tegenkomt zijn Hybrids, SlimJS, SkateJS, Haunted of HyperElement. In de basis doen ze allemaal hetzelfde: overerven van HTMLElement en daar een eigen sausje op aanbrengen. Sommige nemen daarbij inspiratie van een groot framework, bijvoorbeeld Haunted wat sterk lijkt op React Hooks. Op webcomponents.dev is een mooi overzicht te vinden van de meest bekende libraries, die je meteen kan uitproberen in je browser.

Ook zijn er verschillende tools om naast Angular ook React of Vue componenten als web component te bouwen. Die zou ik echter niet aanraden tenzij je het nodig hebt voor een migratietraject, want net als bij Angular Elements verpak je een groot en log framework als web component.

Is een Custom Element nog native als je een library gebruikt?

Dat is een goede vraag. Ik vind van wel. Ongeacht wat voor library je gebruikt, en ook wanneer je niets gebruikt: web components werken altijd met elkaar samen. In de praktijk heb ik gezien dat bijvoorbeeld Polymer, Lit en Hyper-components prima naast elkaar kunnen leven. Data binding tussen deze libraries werkt identiek, en veelal kan de code ook door dezelfde build tooling worden gebouwd. Dat is de kracht van een library op basis van webstandaarden.

Hoe afhankelijk je wilt zijn van een derde partij? Dát is de vraag die je jezelf het beste kan stellen wanneer het gaat om de keuze om wél of geen library te gebruiken. Afhankelijkheden heb je namelijk altijd wel: op je build tooling, analytics en hostingomgeving.

Door te kiezen voor een groot en gevestigd framework leg je een enorme afhankelijkheid op een derde partij voor het hele bestaan van jouw frontend applicatie. Een React component kan je niet zomaar lostrekken en in een Angular codebase zetten. En als Facebook besluit dat React niet meer wordt ondersteund, dan mag je je hele applicatie opnieuw bouwen in een andere techniek. Specifieke principes en logica van React-componenten kan je namelijk niet zomaar naar een ander framework copy-pasten.

Onrealistisch is dit scenario ook niet: AngularJS werd door Google deprecated en vervangen door het nieuwe Angular framework. Echter is dat migratiepad zo complex dat er duizenden organisaties over de hele wereld anno 2021 nog steeds bezig zijn met het migreren van hun frontend! Trouwens: over dat complexe migratieproces heb ik samen met mijn collega Sytze een uitgebreide whitepaper geschreven. Klik hier om hem te bekijken.

Moet ik dan wél een library gebruiken?

Wanneer je geen library gebruikt is het onvermijdelijk dat er tijd gestoken zal worden in het bouwen van generieke utilities, helpers en andere handigheidjes. Op termijn zal je zien dat dit uitgroeit tot een eigen library. Bestaande libraries kunnen deze handigheidjes out-of-the-box aan je geven, waardoor je zelf weer tijd bespaart, en het wiel niet opnieuw uit hoeft te vinden.

Ik adviseer daarom altijd om wél een library te kiezen, maar goed onderzoek te doen naar welke er op dit moment het meeste ondersteuning en toekomstperspectief heeft. Mocht dat toch tegenvallen, en houdt de ondersteuning op voor die library? Dan weet je in ieder geval zeker dat je geen problemen gaat hebben met het integreren van een andere Web Components library: alle web components zijn immers gewoon Custom Elements die leunen op dezelfde webstandaarden.

Welke dan?

Dat is een erg persoonlijke keuze! Gelukkig maken web component libraries het je wel heel makkelijk, omdat ze in de basis allemaal een base class aanleveren die je sugar syntax voert. Ik denk dat alle libraries een goede featureset hebben, en dat het vooral gaat om de keuze in hoe geopinieerd je jouw library wilt hebben. Als je dicht bij de basics wil blijven zou ik naar Lit kijken. Als je keuze afhangt van of de Batteries Included zijn, dan kan je kijken naar Stencil of zelfs Angular Elements.

Ik heb zelf veel ervaring opgedaan met Polymer en Lit tijdens mijn klus via Arcady als Lead Frontend Platform Engineer bij ING Beleggen. En om jullie ook een goed beeld te geven bij hoe je nou echte features bouwt met web components, staat volgende week in deel drie van deze blogpostserie in het thema van Show me the Code! Veelvoorkomende structuren, complexere features en de pros en cons komen daar voorbij. Stay tuned!

Deelnemen aan een Web Components workshop met Wessel als trainer? Geef hier je interesse door. Ook beschikbaar voor developmentteams op locatie. 

 

Inhoudsopgave

  1. Web Components 101
  2. Libraries
  3. Show me the code!
  4. Tooling & Ecosysteem
  5. Scaling up!
  6. The Next Level
Web Components 101
Volgend bericht
Web Components 101