Web-Components: “Piamose er Web”

Danilo Del Fio
Sep 16, 2019 · 11 min read

I custom element sono la risposta alla modernizzazione dell’HTML, cercando, quindi, di creare i TAG mancanti, se così possiamo dire, fornendo agli stessi un significato semantico.

Cosa vuol dire dare una semantica ad un TAG HTML?

Significa che sarà facile capire la funzione di un TAG semplicemente guardandolo, perché esso rappresenterà quello che fa (per esempio un tag <articolo> e non come lo fa, per esempio <div>, specificando magari una classe di stile per customizzarlo in base a dove viene inserito).

Mi piace fare il parallelismo con la programmazione funzionale, in cui invece di codificare come fare una certa cosa (per esempio ciclare un array), gli diciamo cosa fare con ciascun elemento all’interno, per esempio(filtrandolo, mappandolo con una funzione e così via).

Perché usare i web component

Ci sono diverse ragioni che ci dovrebbero spingere a creare web component:

  • Il codice che scriviamo non è per il computer (o almeno non solo): Gli sviluppatori devono porre maggiormente l’accento sulla scrittura di markup più chiari e semantici per diversi motivi: prestazioni migliori, maggiore accessibilità e facilità di manutenzione.
  • Consistenza all’interno della nostra azienda: La suddivisione del codice dell’applicazione front-end in librerie di componenti può garantire la coerenza del brand in tutta l’azienda. Fornisce inoltre un ulteriore vantaggio della possibilità di essere utilizzato da tutti i team, indipendentemente dallo stack tecnologico (perché, come vedremo in seguito, i web components possono essere utilizzati con angular, react, vue e anche vanilla javascript).
  • Risparmio per il Business: Se sei un manager dello sviluppo o un dirigente, i componenti web nativi ti fanno risparmiare denaro. Gli sviluppatori avranno la possibilità di concentrarsi esclusivamente sulla creazione di componenti riutilizzabili nativi, simili ai lego, e di utilizzare questi blocchi in altre applicazioni tra i team.
  • Sviluppatore: Dal punto di vista di uno sviluppatore, i componenti Web nativi aiutano a gestire il progetto in modo più efficiente. Il codice sarà più consumabile. Il codice può essere condiviso più facilmente tra i team (insomma, in questo modo si porta avanti il famoso principio per lo sviluppo del software DRY).
  • Perché è uno standard: I componenti Web sono uno standard che è stato aggiunto dal “World Wide Web Consortium” o W3C. Il W3C imposta o definisce lo standard di funzionalità che i browser possono implementare.
  • Avere delle best practice è sempre una buona idea per definire il tuo stile di sviluppo all’interno e tra i team. I componenti Web nativi dovrebbero funzionare ovunque. Si basano su standard web e rappresentano il futuro della piattaforma.

Benefici

  • Dichiarazione: puoi dichiarare facilmente i componenti sulla tua pagina che sono pronti per l’uso.
  • Composizione: puoi comporre le applicazioni usando blocchi di codice più piccoli, con Shadow DOM.
  • Riusabilità: è possibile importare, utilizzare e riutilizzare elementi nelle applicazioni.
  • Manutenibilità: il codice riutilizzabile compartimentato è il modo migliore per mantenere la leggibilità del codice; riduce le dimensioni complessive dell’applicazione e semplifica il debug.
  • Estensibilità: elementi del browser o componenti Web personalizzati possono essere estesi con le custom element API.
  • Scoping: Shadow DOM fornisce scoping DOM e CSS in cui gli stili non perdono e il componente DOM è tutto locale. Definisci api elemento all’interno del tuo componente e non rientra nell’ambito globale
  • Interoperabilità: i componenti Web nativi sono interoperabili al livello più basso del browser, ovvero DOM.
  • Produttività: l’utilizzo di componenti già costruiti e l’iterazione su di essi ci consentono di sviluppare più velocemente e di essere più produttivi.
  • Accessibilità: utilizzando ed estendendo gli elementi del browser esistenti, viene fornita l’accessibilità del browser predefinita.

Specifiche

I web component sono costruiti sulla base di 4 tecnologie separate, utilizzate insieme:

  • Custom Element. (EX: <ddf-dialog>)
  • Shadow DOM: Permette di isolare CSS e javascript
  • Template HTML: renderizzati solo quando chiamati.
  • Moduli ES: raggruppa una raccolta di piccole funzionalità indipendenti in una libreria.

Queste quattro tecnologie sono ciò che costituiscono la specifica dei Web component.

Compatibilità

I Web component sono supportati quasi da tutti i browser moderni. Alcuni browser sono ancora in fase di aggiornamento per supportare tutti gli standard per i Web component. Nel frattempo, i polyfill simulano il più possibile le funzionalità del browser mancante.

export class TextSentence {
constructor(phrase, type) {
this.phrase = phrase;
this.type = type;
}

emitSentence() {
return `I ${this.type} sono ${this.phrase}!`;
}
}

export function emitSentenceFunction(phrase){
return `I web component sono ${phrase}!`;
}

per poter utilizzare la classe o la funzione esportata, basta scrivere:

// import a classimport { TextSentence } from './lib-class-example.js';const text = new TextSentence('stupendi', 'web component');const sentence= text.emitSentence();// --> I web component sono stupendi!;// import a functionimport {emitSentenceFunction} from './lib-function-example.js'const text = greet('Web Component');// --> I Web Component sono stupendi!;

Vediamo ora come creare i nostri Custom Element e come, il browser, ci supporta in questo.

L’oggetto globale window espone una proprietà customElements che ci dà accesso a un oggetto CustomElementRegistry.

Cosa ci mette a disposizione l’oggetto CustomElementRegistry

  • define() usato per definire un nuovo elemento personalizzato
  • get() utilizzato per ottenere il costruttore di un custom element (restituisce indefinito se non esistente)
  • upgrade() per aggiornare un custom element
  • whenDefined() utilizzato per ottenere il costruttore di un custom element. E’ simile alla get(), ma restituisce invece una promise, che si risolve quando l’elemento è disponibile.

Con tutto quello che abbiamo visto finora, siamo in grado di creare il nostro primo Custom Element.

Definiamo, quindi, la nostra classe

class DDFTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<h1>My Custom Title!</h1>`
}
}

Una caratteristica dei custom element è che this all’interno di una definizione di classe si riferisce all’elemento DOM stesso, cioè l’istanza della classe. Nel nostro esempio, questo si riferisce a <ddf-title> (questo sarà il nome che daremo al neonato tag).

Ci sono diversi aspetti che saltano agli occhi nel codice che abbiamo appena scritto.

Innanzitutto vediamo che la nostra classe estende l’interfaccia HTMLElement. Questo elemento anche se non ce ne accorgiamo è l’elemento base di tutti gli elementi (TAG) che usiamo nelle nostre pagine HTML.

Viene da se che, per creare un nuovo elemento del DOM, dobbiamo passare attraverso la creazione di un HTMLElement, ed è per questo che la prima istruzione del nostro costrutto re DEVE essere una chiamata al costruttore dell’elemento che stiamo estendendo, attraverso il comando super().

Nei linguaggi di programmazione orientati agli oggetti, un’interfaccia definisce un insieme di metodi che una Classe deve includere per implementare l’interfaccia (altrimenti, se alla Classe mancano i metodi richiesti, il codice fallirà e l’interfaccia genererà un errore).

Le interfacce sono utili per assicurarsi che gli sviluppatori utilizzino la corretta implementazione di un’API.

In JavaScript non ci sono vere e proprie funzionalità “classiche” orientate agli oggetti, ma attraverso un uso intelligente del linguaggio è possibile emulare un’interfaccia da utilizzare con un’API JavaScript.

L’istruzione successiva this.attachShadow(), ci permette di creare un “sotto DOM”, chiamato shadowRoot, a cui agganceremo i tag del nostro componente.

L’interfaccia ShadowRoot della Shadow DOM API è il nodo radice di una sotto-struttura DOM che viene visualizzata separatamente dall’albero DOM principale di un documento.

È possibile recuperare un riferimento alla radice shadow di un elemento utilizzando la sua proprietà Element.shadowRoot, a condizione che sia stato creato utilizzando Element.attachShadow() con l’opzione mode impostata su ‘open’.

A questo punto possiamo utilizzare la proprietà innerHTML sul nostro shadowRoot che permette di ottenere o impostare il markup HTML o XML contenuto all’interno dell’elemento.

Dopo aver creato il nostro nuovo elemento sarà necessario aggiungerlo al CustomElementRegistry, utilizzando la funzione define() che ci viene messa a disposizione:

window.customElements.define(‘ddf-title’, DDFTitle);

Non possiamo usare tag a chiusura automatica: non è consentito dallo standard.

Un’altra cosa da tenere bene a mente è che i TAG che creiamo con customElement.define deve contenere un nome col trattino, per esempio: ddf-toolbar, ddf-card, ecc…

Nella definizione del nostro elemento, quando agganciamo il nostro HTML alla shadowRoot, per definirne la forma ed il contenuto, possiamo definirne lo stile attraverso l’aggiunta di un tag <style> che ci permetterà, quindi, di customizzare il look and feel del componente che stiamo creando e, come detto in precedenza, rimarrà confinato all’interno del nostro componente, isolandolo completamente dello stile definito all’esterno.


this.shadowRoot.innerHTML = `
<style>
h1 {
font-size: 7rem;
color: #000;
font-family: Helvetica;
text-align: center;
}
</style>
<h1>My Custom Title!</h1>
`

La stessa cosa la possiamo definire per i javascript che aggiungiamo al nostro custom element. Per esempio se vogliamo agganciare un listener al nostro componente, basterà scrivere:


this.attachShadow({ mode: ‘open’ });
this.shadowRoot.innerHTML = `<h1>My Custom Title!</h1>`
this.addEventListener(‘click’, e => {
alert(‘clicked!’)
})

this si riferisce sempre all’istanza della nostra classe e quindi al componente che abbiamo creato.

Per definire il nostro frammento HTML che costituisce il custom element lo stile ad esso associato, possiamo (per pulizia del codice lo consiglio), creare un template HTML e agganciarlo poi, al nostro shadowRoot. L’utilizzo dei template, oltre ad essere un modo più pulito di scrivere l’HTML che costituirà il nostro componente, ha il vantaggio di venire agganciato al DOM, quando effettivamente ce n’è bisogno, ovvero quando andiamo a richiamarlo.

<template id=”ddf-title-template”>
<style>
h1 {
font-size: 7rem;
color: #000;
font-family: Helvetica;
text-align: center;
}
</style>
<h1>My Personal Title!</h1>
</template>
<ddf-title></ddf-title>

Evidentemente, dopo creato, per utilizzarlo, dobbiamo agganciarlo alla nostra shadowRoot

this.attachShadow({ mode: ‘open’ })
const tmpl = document.querySelector(‘#ddf-title-template’)
this.shadowRoot.appendChild(tmpl.content.cloneNode(true))

Invece di definire HTML e CSS in una stringa JavaScript, quindi, è possibile utilizzare un tag “template” in HTML e assegnargli un ID.

HTML Import

In realtà gli elementi <template> possono essere importati da un altro documento tramite HTML Import, insieme al codice Javascript che definirà il nostro custom element:

<link rel = “import” src = “my-custom-element.html”>

<custom-element> </ custom-element>

Quindi non è necessario che sia incluso in ogni documento HTML.

Le HTML Import, purtroppo, sono implementate solo in Chrome e Opera. Se si desidera utilizzarli con Firefox e Safari, è necessario utilizzare il polyfill.

D’altra parte e per il momento, Mozilla e Apple non intendono implementare gli HTML Import in modo nativo nei rispettivi browser. Pertanto raccomandano di definire elementi personalizzati con moduli Javascript puri (con import o <script src = “…”>) e di promuovere invece stringhe attraverso template literlals (mediante l’uso di back-tic, che danno la possibilità di scrivere stringhe multi-linea e definire variabili e funzioni), ma a volte sono più complicate da codice in un IDE (a causa della loro rappresentazione di stringa).

Forse in futuro i moduli HTML standard saranno adottati da tutti i browser e <template> tornerà sotto i riflettori, ma per ora sarebbe meglio non usare questo strumento.

Lifecycle Hooks

Negli esempi che ho fatto precedentemente ho sottolineato la necessità di inserire un costruttore che permette di creare un’istanza del nostro componente.

Il costruttore è solo uno dei lifecycle hooks che vengono chiamati in momenti specifici del ciclo di vita del nostro web component.

I metodi di cui parlo sono:

constructor()
// chiamato quando viene creata o aggiornata un’istanza dell’elemento.

Utile per inizializzare lo stato, impostare listener di eventi o creare il nostro shadowDOM. Ci sono anche delle restrizioni e delle linee guida su ciò che possiamo scrivere nel costruttore.

Alcuni esempi di restrizioni, sono:

  • La prima istruzione scritta all’interno del costruttore deve essere una chiamata senza parametri a super(), in questo modo stabiliamo la catena di prototipi corretta .
  • Un’istruzione return non deve apparire in nessun punto all’interno del corpo del costruttore, a meno che non sia un semplice ritorno anticipato (return o return this).
  • Il costruttore non deve utilizzare i metodi document.write() o document.open()
connectedCallback():
// Chiamato ogni volta che l’elemento viene inserito nel DOM.

Utile per l’esecuzione del codice di installazione, come il recupero di risorse o il rendering. Generalmente, dovresti provare a ritardare il lavoro fino a questo momento.

disconnectedCallback():
// Chiamato ogni volta che l’elemento viene rimosso dal DOM.

Utile per l’esecuzione di codice che permette di fare “la pulizia” dalle risorse utilizzate così via.

attributeChangedCallback(attrName, oldVal, newVal):
// Chiamato quando un attributo osservato è stato aggiunto, rimosso,
// aggiornato o sostituito.

Chiamato anche per i valori iniziali quando un elemento viene creato dal parser o aggiornato.

Il browser chiama attributeChangedCallback() per tutti gli attributi autorizzati attraverso un array chiamato observedAttributes.

static get observedAttributes() {
return [‘name’, ‘color’];
}

In sostanza, si tratta di un’ottimizzazione delle prestazioni. Quando gli utenti cambiano un attributo comune come stile o classe, non vuoi farlo essere spammato con tonnellate di callback

adoptedCallback():
// L’elemento personalizzato è stato spostato in un nuovo documento.

Il metodo adoptedCallback() viene chiamato quando un elemento personalizzato viene spostato da un documento HTML a un altro con il metodo adopNode().

È necessario implementare questo metodo solo se l’elemento personalizzato verrà utilizzato in un contesto multi-documento e quando è necessario eseguire alcune operazioni speciali che non devono essere eseguite in connectedCallback(), ad esempio le interazioni con il documento proprietario, il documento principale, o altri elementi.

Esempio: se si desidera visualizzare un’interazione speciale quando l’elemento viene aggiunto al documento, ma non quando è originariamente al suo interno, non lo si farà in constructor() o in connectedCallback(), ma è possibile farlo in adoptedCallback().

Fetch API

Volevo fare un semplice accenno ad un altro strumento che ci mette a disposizione il browser (come del resto le specifiche per creare i nostri web component), perché i nostri componenti non saranno fine a se stessi, ma potrebbero aver bisogno di interagire con delle risorse accessibili attraverso degli endpoint sul nostro server di backend.

Le Fetch API, permettono proprio questo tipo di interazione, senza la necessità di nessuna libreria per poterlo fare.

Per esempio, per eseguire una GET, basterebbe utilizzare :

let response = await fetch(“http://myapi.com/data");
let json = await response.json();

La POST, invece, prevede l’invio di dati verso il server per la creazione di risorse:

let data = { comment: ‘Ciao’, author: ‘RightSide’ };
let response = await fetch(“https://someapi.com/comments", {
method: ‘POST’,
body: JSON.stringify(data),
headers: {
‘Content-Type’: ‘application/json’
}
});
let json = await response.json();

Una considerazione da tenere a mente nel caso in cui si utilizzino queste API per interagire col server, è che tali API non forniscono uno status di errore in caso che qualcosa vada male (lo fanno solo in rari casi di problemi di networking), ma lo status restituito sarà sempre 200 (un errore viene anch’esso visto come qualcosa che viene creato).

Sarà, quindi, nostra premura dover intercettare la risposta dal server e comportarci di conseguenza, per esempio lanciando noi un eccezione specificando un Error:

try {
let response = await fetch(“https://someapi.com/data");
if(!response.ok){
throw new Error(response.statusText);
}
let json = await response.json();
} catch (err) {
console.log(err);
}

Si noti che senza le importazioni HTML è ancora possibile importare alcuni documenti HTML con fetch (fetch è un API standard messa a disposizione dai browser):

fetch(“template.html”)
.then (stream => stream.text ())
.then (text =>
customElements.define (“my-title”, class DDFTitle extends
HTMLElement {
constructor () {
super()
this.attachShadow ({mode: ‘open’}).innerHTML = text
}
})
)

Esempio su Codepen

Su codepen ho creato un piccolo esempio di Componente a partire da un esempio di CARD trovato su codepen stesso. Come si può vedere è possibile definire dei parametri (attributi), che permettono di customizzare il comportamento del componente creato.

Ascoltami su Spotify

Ho creato una puntata del mio podcast su Spotify. Mi puoi ascoltare seguendo il link:

Conclusioni

Dopo anni dalla loro prima apparizione, i web component, hanno finalmente conquistato la considerazione che, secondo me, meriterebbero, perché mettono a disposizione dei modi semplici e veloci per creare i nostri Tag personalizzati, ma che verranno riconosciuti in maniera nativa dal browser nel quale verranno fatti girare.

Questo è una freccia enorme al nostro arco che ci permette, quindi, di creare veramente qualcosa di riutilizzabile, cross tecnologia e cross team e che va sicuramente incontro alle esigenze del business e aziendali, unendo un risparmio in termini di tempo e denaro, nonché uno strumento che permetta di mantenere il brand aziendale in tutti i progetti in cui l’azienda sarà coinvolta.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade