Tutti noi abbiamo progetti a cui preferiremmo non lavorare. Il codice è diventato ingestibile, lo scopo si è evoluto, sono state fatte correzioni veloci su altre correzioni e la struttura è crollata sotto il suo peso di spaghetti code. Creare codice può essere un affare disordinato.

I progetti beneficiano dell’utilizzo di moduli semplici e indipendenti con una sola responsabilità. Il codice modulare è incapsulato, quindi c’è meno bisogno di preoccuparsi dell’implementazione. Finché si sa cosa produrrà un modulo quando gli viene dato un insieme di input, non si ha necessariamente bisogno di capire come ha raggiunto quell’obiettivo.

Applicare concetti modulari a un singolo linguaggio di programmazione è semplice, ma lo sviluppo web richiede un mix diversificato di tecnologie. I browser analizzano HTML, CSS e JavaScript per rendere il contenuto della pagina, gli stili e le funzionalità.

Non sempre si combinano facilmente perché:

  • Il codice correlato può essere suddiviso in tre o più file, e
  • Gli stili globali e gli oggetti JavaScript possono interferire l’uno con l’altro in modi inattesi.

Questi problemi si aggiungono a quelli incontrati da runtime di linguaggi, framework, database e altre dipendenze utilizzate sul server.

Guarda la nostra video guida ai componenti web

Cosa Sono i Web Component?

Un Web Component è un modo per creare un blocco di codice incapsulato e a responsabilità singola che può essere riutilizzato in qualsiasi pagina.

Considerate il tag HTML <video> . Dato un URL, un utente può utilizzare controlli come play, pausa, indietro, avanti e regolare il volume.

Lo stile e la funzionalità sono forniti, anche se è possibile apportare modifiche utilizzando vari attributi e chiamate all’API JavaScript. Qualsiasi numero di elementi <video> può essere collocato all’interno di altri tag, e non entreranno in conflitto.

E se avete bisogno di una vostra funzionalità custom? Ad esempio, un elemento che mostri il numero di parole nella pagina? Non c’è un tag HTML <wordcount> (per il momento).

Framework come React e Vue.js permettono agli sviluppatori di creare componenti web dove il contenuto, lo stile e le funzionalità possono essere definiti in un singolo file JavaScript. Questi risolvono molti problemi di programmazione complessi, ma bisogna tenere a mente che:

  • Dovete imparare a utilizzare quel framework e aggiornare il vostro codice man mano che evolve.
  • Un componente scritto per un framework è raramente compatibile con un altro.
  • I framework aumentano e diminuiscono di popolarità. Diventerete dipendenti dai capricci e dalle priorità del team di sviluppo e degli utenti.
  • I Web Component standard possono aggiungere funzionalità al browser, che è difficile raggiungere con il solo JavaScript (come lo Shadow DOM).

Per fortuna, i concetti popolari introdotti nelle librerie e nei framework di solito si fanno strada negli standard web. C’è voluto del tempo, ma i Web Component sono arrivati.

Breve Storia dei Web Component

Dopo molte false partenze, il concetto di Web Component standard è stato introdotto per la prima volta da Alex Russell alla conferenza Fronteers nel 2011. La libreria Polymer di Google (un polyfill basato sulle proposte correnti) è arrivata due anni dopo, ma in Chrome e Safari le prime implementazioni non sono apparse fino al 2016.

I vendor dei browser si sono presi del tempo per negoziare i dettagli, ma i Web Component sono stati aggiunti a Firefox nel 2018 e Edge nel 2020 (quando Microsoft è passata al motore Chromium).

Comprensibilmente, pochi sviluppatori sono stati disposti o sono riusciti ad adottare i Web Component, ma abbiamo finalmente raggiunto un buon livello di supporto da parte dei browser con API stabili. Non tutto è perfetto, ma sono un’alternativa sempre più valida ai componenti basati su framework.

Anche se non siete ancora disposti a liberarvi del vostro framework preferito, i Web Component sono compatibili con ogni framework e le API saranno supportate per gli anni a venire.

Ecco un elenco di repository di componenti web pre-costruiti disponibili per tutti:

… ma scrivere il proprio codice è più divertente!

Questo tutorial offre un’introduzione completa ai componenti web scritti senza un framework JavaScript. Vi spiegheremo cosa sono e come adattarli ai vostri progetti web. Avrete bisogno di qualche conoscenza di HTML5, CSS e JavaScript.

Introduzione ai Web Component

I componenti web sono elementi HTML personalizzati come <hello-world></hello-world>. Il nome deve contenere un trattino per non andare in conflitto con elementi supportati ufficialmente nella specifica HTML.

Dovete definire una classe ES2015 per controllare l’elemento. Può avere qualsiasi nome, ma HelloWorld è tra i più comuni. Deve estendere l’interfaccia HTMLElement, che rappresenta le proprietà e i metodi predefiniti di ogni elemento HTML.

Nota: Firefox permette di estendere specifici elementi HTML come HTMLParagraphElement, HTMLImageElement o HTMLButtonElement. Questo non è supportato in altri browser e non permette di creare uno Shadow DOM.

Per fare qualcosa di utile, la classe richiede un metodo chiamato connectedCallback(), che viene invocato quando l’elemento viene aggiunto a un documento:

class HelloWorld extends HTMLElement {

  // connect component
  connectedCallback() {
    this.textContent = 'Hello World!';
  }

}

In questo esempio, il testo dell’elemento è impostato su “Hello World”.

La classe deve essere registrata con il CustomElementRegistry per essere definita come gestore per un elemento specifico:

customElements.define( 'hello-world', HelloWorld );

Ora, quando il vostro codice JavaScript viene caricato, il browser associa l’elemento <hello-world> alla vostra classe HelloWorld (ad esempio <script type="module" src="./helloworld.js"></script> ).

Ora avete un elemento personalizzato!

Dimostrazione CodePen

Questo componente può essere stilizzato nei CSS come qualsiasi altro elemento:

hello-world {
  font-weight: bold;
  color: red;
}

Aggiungere gli Attributi

Questo componente non è molto utile perché viene emesso sempre lo stesso testo. Come qualsiasi altro elemento, possiamo aggiungere attributi HTML:

<hello-world name="Craig"></hello-world>

Questo potrebbe sovrascrivere il testo in modo che venga visualizzato “Hello Craig!”” Per farlo, si può aggiungere una funzione constructor() alla classe HelloWorld, che viene eseguita quando ogni oggetto viene creato. Essa deve:

  1. invocare il metodo super() per inizializzare l’HTMLElement padre e
  2. fare altre inizializzazioni. In questo caso, definiremo una proprietà name, impostata di default su “World”:
class HelloWorld extends HTMLElement {

  constructor() {
    super();
    this.name = 'World';
  }

  // more code...

Il vostro componente si preoccupa solo dell’attributo name. Una proprietà statica observedAttributes() dovrebbe restituire un array di proprietà da osservare:

// component attributes
static get observedAttributes() {
  return ['name'];
}

Il metodo attributeChangedCallback() viene invocato quando un attributo viene definito nell’HTML o cambiato tramite JavaScript. Viene passato il nome della proprietà, il vecchio valore e il nuovo valore:

// attribute change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return;
  this[ property ] = newValue;

}

In questo esempio, solo la proprietà name verrebbe aggiornata, ma potreste aggiungere altre proprietà se necessario.

Infine, è necessario modificare il messaggio nel metodo connectedCallback():

// connect component
connectedCallback() {

  this.textContent = `Hello ${ this.name }!`;

}

Dimostrazione CodePen

Metodi Lifecycle

Il browser invoca automaticamente sei metodi durante il ciclo di vita dello stato del Componente Web. Ecco la lista completa, anche se avete già visto i primi quattro negli esempi precedenti:

constructor()

Viene invocato quando il componente viene inizializzato per la prima volta. Deve invocare super() e può impostare qualsiasi valore di default o eseguire altri processi di pre-rendering.

static observedAttributes()

Restituisce un array di attributi che il browser dovrà osservare.

attributeChangedCallback(propertyName, oldValue, newValue)

Invocato ogni volta che viene cambiato un attributo osservato. Quelli definiti in HTML vengono passati immediatamente, ma possono essere modificati via JavaScript:

document.querySelector('hello-world').setAttribute('name', 'Everyone');

Quando questo accade, il metodo potrebbe aver bisogno di innescare un re-rendering.

connectedCallback()

Questa funzione viene invocata quando il Web Component viene aggiunto a un Document Object Model. Dovrebbe eseguire qualsiasi rendering richiesto.

disconnectedCallback()

Viene invocato quando il Web Component viene rimosso da un Document Object Model. Questo può essere utile se avete bisogno di fare pulizia, come rimuovere lo stato memorizzato o interrompere le richieste Ajax.

adoptedCallback()

Questa funzione viene invocata quando un Web Component viene spostato da un documento all’altro. Si può trovare un impiego per questo, anche se ho fatto fatica a pensare a qualche caso concreto!

Come i Web Component Interagiscono con Altri Elementi

I Web Component offrono alcune funzionalità uniche che non troverete nei framework JavaScript.

Lo Shadow DOM

Anche se il Web Component che abbiamo creato sopra funziona, non è immune da interferenze esterne e potrebbe essere modificato da CSS o JavaScript. Allo stesso modo, gli stili che definite per il vostro componente potrebbero trapelare e influenzare gli altri.

Lo Shadow DOM risolve questo problema di incapsulamento, attaccando un DOM separato al componente web con:

const shadow = this.attachShadow({ mode: 'closed' });

La modalità può essere:

  1. “open” – JavaScript nella pagina esterna può accedere al DOM ombra (con Element.shadowRoot) o
  2. “closed” – si può accedere al DOM ombra solo all’interno del componente web.

Lo Shadow DOM può essere manipolato come qualsiasi altro elemento del DOM:

connectedCallback() {

  const shadow = this.attachShadow({ mode: 'closed' });

  shadow.innerHTML = `
    <style>
      p {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>

    <p>Hello ${ this.name }!</p>`;

}

Il componente ora rende il testo “Hello” all’interno di un elemento <p> e lo stilizza. Non può essere modificato da JavaScript o CSS al di fuori del componente, anche se alcuni stili come il font e il colore sono ereditati dalla pagina perché non sono stati definiti esplicitamente.

Dimostrazione CodePen

Gli stili applicati a questo componente web non possono influenzare altri paragrafi della pagina o anche altri componenti <hello-world>.

Si noti che il selettore CSS :host può stilizzare l’elemento esterno <hello-world> dall’interno del componente web:

:host {
  transform: rotate(180deg);
}

Potete anche impostare stili da applicare quando l’elemento usa una classe specifica, ad esempio <hello-world class="rotate90">:

:host(.rotate90) {
  transform: rotate(90deg);
}

Template HTML

Definire l’HTML all’interno di uno script può diventare poco pratico per i componenti web più complessi. Un template vi permette di definire un blocco di HTML nella vostra pagina che può essere utilizzato dal vostro Web Component. Questo presenta diversi vantaggi:

  1. Potete modificare il codice HTML senza dover riscrivere le stringhe nel vostro codice JavaScript.
  2. I componenti possono essere personalizzati senza dover creare classi JavaScript separate per ogni tipo.
  3. È più facile definire l’HTML in HTML – e può essere modificato sul server o sul client prima del rendering del componente.

I modelli sono definiti in un tag <template> , ed è pratico assegnare un ID in modo da poter fare riferimento all’interno della classe del componente. Questo esempio mostra tre paragrafi per visualizzare il messaggio “Hello”:

<template id="hello-world">

  <style>
    p {
      text-align: center;
      font-weight: normal;
      padding: 0.5em;
      margin: 1px 0;
      background-color: #eee;
      border: 1px solid #666;
    }
  </style>

  <p class="hw-text"></p>
  <p class="hw-text"></p>
  <p class="hw-text"></p>

</template>

La classe del Web Component può accedere a questo template, prelevare il suo contenuto e clonare gli elementi per creare un frammento DOM unico ovunque venga usato:

const template = document.getElementById('hello-world').content.cloneNode(true);

Il DOM può essere modificato e aggiunto direttamente allo Shadow DOM:

connectedCallback() {

  const

    shadow = this.attachShadow({ mode: 'closed' }),
    template = document.getElementById('hello-world').content.cloneNode(true),
    hwMsg = `Hello ${ this.name }`;

  Array.from( template.querySelectorAll('.hw-text') )
    .forEach( n => n.textContent = hwMsg );

  shadow.append( template );

}

Dimostrazione CodePen

Slot di Template

Gli slot permettono di personalizzare un template. Supponiamo che vogliate utilizzare il vostro componente web <hello-world> ma posizionare il messaggio all’interno di un’intestazione <h1> nello Shadow DOM. Potreste scrivere questo codice:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>

</hello-world>

(Notate l’attributo slot).

Potreste opzionalmente voler aggiungere altri elementi come un altro paragrafo:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Gli slot possono ora essere implementati all’interno del vostro template:

<template id="hello-world">

  <slot name="msgtext" class="hw-text"></slot>

  <slot></slot>

</template>

Un attributo slot dell’elemento impostato su “msgtext” (l’<h1>) viene inserito nel punto in cui c’è uno <slot> chiamato “msgtext.” All’elemento <p> non è assegnato un nome di slot, ma viene utilizzato nel successivo <slot> senza nome disponibile. In effetti, il template diventa:

<template id="hello-world">

  <slot name="msgtext" class="hw-text">
    <h1 slot="msgtext">Hello Default!</h1>
  </slot>

  <slot>
    <p>This text will become part of the component.</p>
  </slot>

</template>

In realtà non è così semplice. Un elemento <slot> nel DOM Shadow punta agli elementi inseriti. È possibile accedervi solo individuando uno <slot> e poi utilizzando il metodo .assignedNodes() per restituire un array di figli interni. Il metodo connectedCallback() aggiornato:

connectedCallback() {

  const
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name }`;

  // append shadow DOM
  shadow.append(
    document.getElementById('hello-world').content.cloneNode(true)
  );

  // find all slots with a hw-text class
  Array.from( shadow.querySelectorAll('slot.hw-text') )

    // update first assignedNode in slot
    .forEach( n => n.assignedNodes()[0].textContent = hwMsg );

}

Dimostrazione CodePen

Inoltre, non potete dare direttamente lo stile agli elementi inseriti, anche se potete mirare a slot specifici all’interno del vostro componente web:

<template id="hello-world">

  <style>
    slot[name="msgtext"] { color: green; }
  </style>

  <slot name="msgtext" class="hw-text"></slot>
  <slot></slot>

</template>

Gli slot dei template sono un po’ insoliti, ma un vantaggio è che il vostro contenuto sarà mostrato se JavaScript non può essere eseguito. Questo codice mostra un titolo e un paragrafo di default che vengono sostituiti solo quando la classe del Web Component viene eseguita con successo:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Pertanto, si potrebbe implementare qualche forma di miglioramento progressivo – anche se è solo un messaggio “Ti serve JavaScript”!

Shadow DOM Dichiarativo

Gli esempi precedenti costruiscono uno Shadow DOM usando JavaScript. Questa rimane l’unica opzione, ma è in fase di sviluppo uno Shadow DOM dichiarativo sperimentale per Chrome. Questo permette il Server-Side Rendering ed evita qualsiasi spostamento di layout o flash di contenuto non stilizzato.

Il seguente codice viene rilevato dal parser HTML, che crea un DOM Shadow identico a quello che avete creato nella precedente sezione (dovreste aggiornare il messaggio come necessario):

<hello-world name="Craig">

  <template shadowroot="closed">
    <slot name="msgtext" class="hw-text"></slot>
    <slot></slot>
  </template>

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

La funzionalità non è disponibile in nessun browser e non c’è garanzia che arriverà in Firefox o Safari. Potete trovare maggiori informazioni sullo Shadow DOM dichiarativo, e un semplice polyfill, ma sappiate che l’implementazione potrebbe cambiare.

Eventi dello Shadow DOM

Il vostro Web Component può attaccare eventi a qualsiasi elemento nello Shadow DOM proprio come fareste nel DOM della pagina, ad esempio per ascoltare gli eventi di clic su tutti i figli interni:

shadow.addEventListener('click', e => {

  // do something

});

A meno che non fermiate la propagazione con StopPropagation, l’evento si propagherà nel DOM della pagina, ma l’evento sarà retargettizzato. Quindi, sembra provenire dal vostro elemento personalizzato invece che da elementi al suo interno.

Utilizzare i Web Component in Altri Framework

Qualsiasi Web Component che create funzionerà in tutti i framework JavaScript. Nessuno di questi conosce o si preoccupa degli elementi HTML – il vostro componente <hello-world> sarà trattato in modo identico a una <div> e inserito nel DOM dove la classe si attiverà.

custom-elements-everywhere.com fornisce un elenco di framework e note sui Web Component. La maggior parte sono completamente compatibili, anche se React.js presenta delle particolarità. È possibile utilizzare <hello-world> in JSX:

import React from 'react';
import ReactDOM from 'react-dom';
import from './hello-world.js';

function MyPage() {

  return (
    <>
      <hello-world name="Craig"></hello-world> 
    </>
  );

}

ReactDOM.render(<MyPage />, document.getElementById('root'));

… ma:

  • React può passare solo tipi di dati primitivi agli attributi HTML (non array o oggetti)
  • React non può ascoltare gli eventi dei componenti web, quindi devi collegare manualmente i vostri gestori.

Critiche e Problemi dei Web Component

I Web Component sono migliorati notevolmente, ma alcuni aspetti possono essere difficili da gestire.

Difficoltà di Styling

Lo styling dei componenti web comporta alcune difficoltà, specialmente se si vuole sovrascrivere gli stili a scopo. Ci sono molte soluzioni:

  1. Evitare di utilizzare lo Shadow DOM. Potreste aggiungere del contenuto direttamente al vostro elemento personalizzato, anche se qualsiasi altro codice JavaScript potrebbe accidentalmente o maliziosamente cambiarlo.
  2. Utilizzare le classi :host. Come abbiamo visto sopra, lo scoped CSS può applicare stili specifici quando viene applicata una classe all’elemento personalizzato.
  3. Controllare le proprietà personalizzate di CSS (variabili). Le proprietà personalizzate si riversano nei componenti web. Così, se il vostro elemento usa var(--my-color), potete impostare --my-color in un container esterno (come :root), e questo sarà utilizzato.
  4. Sfruttate le parti shadow. Il nuovo selettore ::part() può stilizzare un componente interno che ha un attributo part. Ad esempio, <h1 part="heading"> dentro un componente <hello-world> può essere stilizzato con il selettore hello-world::part(heading).
  5. Passare una stringa di stili. Potete passarli come attributo da applicare all’interno di un blocco < style>.

Nessuno di questi è ideale, e dovrete pianificare attentamente il modo in cui gli altri utenti possono personalizzare il vostro componente web.

Input Ignorati

Qualsiasi campo <input>, <textarea> o <select> nello Shadow DOM non è automaticamente associato al modulo che lo contiene. I primi utilizzatori di Web Component aggiungevano campi nascosti al DOM della pagina oppure utilizzavano l’interfaccia FormData per aggiornare i valori. Nessuno dei due è particolarmente pratico e rompe l’incapsulamento dei Web Component.

La nuova interfaccia ElementInternals permette ad un componente web di agganciarsi ai moduli in modo da poter definire valori e validità personalizzati. È implementata in Chrome, ma un è disponibile polyfill per gli altri browser.

Come dimostrazione, si creerà un componente base <input-age name="your-age"></input-age> . La classe deve avere un valore statico formAssociated impostato a true e, opzionalmente, può essere invocato un metodo formAssociatedCallback() quando il modulo esterno viene associato:

// <input-age> web component
class InputAge extends HTMLElement {

  static formAssociated = true;

  formAssociatedCallback(form) {
    console.log('form associated:', form.id);
  }

Il costruttore deve ora eseguire il metodo attachInternals(), che permette al componente di comunicare con il modulo e con altro codice JavaScript che vuole ispezionare il valore o la validazione:

  constructor() {

    super();
    this.internals = this.attachInternals();
    this.setValue('');

  }

  // set form value

  setValue(v) {

    this.value = v;

    this.internals.setFormValue(v);

  }

Il metodo setFormValue() di ElementInternal imposta il valore dell’elemento per il form padre inizializzato qui con una stringa vuota (può anche essere passato un oggetto FormData con più coppie nome/valore). Altre proprietà e metodi includono:

  • form: il modulo padre
  • labels: un array di elementi che etichettano il componente
  • Opzioni Constraint Validation API come willValidate, checkValidity e validationMessage

Il metodo connectedCallback() crea uno Shadow DOM come prima, ma deve anche monitorare il campo per le modifiche, in modo che possa essere eseguito setFormValue():

  connectedCallback() {

    const shadow = this.attachShadow({ mode: 'closed' });

    shadow.innerHTML = `
      <style>input { width: 4em; }</style>
      <input type="number" placeholder="age" min="18" max="120" />`;

    // monitor input values
    shadow.querySelector('input').addEventListener('input', e => {
      this.setValue(e.target.value);
    });

  }

Ora potete creare un modulo HTML con questo componente web che agisce in modo simile agli altri campi dei moduli:

<form id="myform">

  <input type="text" name="your-name" placeholder="name" />

  <input-age name="your-age"></input-age>

  <button>submit</button>

</form>

Funziona, ma è anche vero che sembra un po’ contorto.

Verificatelo nella dimostrazione CodePen

Per maggiori informazioni, fate riferimento a questo articolo sui controlli di form con maggiori capacità.

Riepilogo

I Web Component hanno faticato a guadagnare consenso e adozione in un momento in cui i framework JavaScript sono cresciuti in statura e capacità. Se si proviene da React, Vue.js o Angular, i Web Component possono sembrare complessi e ingombranti, specialmente quando mancano funzionalità come il data-binding e la gestione dello stato.

Ci sono dei problemi da risolvere, ma il futuro dei Web Component è luminoso. Sono indipendenti dal framework, sono leggeri, veloci e possono implementare funzionalità che sarebbero impossibili con il solo JavaScript.

Una decina di anni fa, pochi avrebbero affrontato un sito senza jQuery, ma i vendor dei browser hanno preso le parti eccellenti e aggiunto alternative native (come querySelector). Lo stesso accadrà per i framework JavaScript, e i Web Component sono quel primo timido passo.

Avete qualche domanda su come utilizzare i componenti web? Parliamone nella sezione dei commenti!

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.