Riguardando l'evoluzione del codice del Web component che abbiamo
realizzato finora, ci rendiamo conto che siamo partiti da un approccio in
cui il markup del componente era rappresentato da stringhe fisse e siamo
approdati ad un approccio in cui tutto è creato dinamicamente tramite le
API del DOM.
L'approccio basato su stringhe offriva una maggiore chiarezza in quando
presentava la struttura sotto forma di markup, ma purtroppo non era
flessibile come ci servirebbe.
L'approccio basato sull'utilizzo delle API del DOM è flessibile, ma non
offre la chiarezza del markup diretto.
Un approccio alternativo nella costruzione della struttura di un componente
che ci consente di mettere insieme flessibilità e chiarezza è quello basato
sull'uso dei template. Questa funzionalità è basata sul tag <template>
, che non è strettamente legato ai Web
component ma il suo impiego nella definizione della loro struttura è molto
comodo, soprattutto in presenza di componenti complessi.
In estrema sintesi, il tag <template>
consente di
definire un blocco di markup che non viene renderizzato subito, ma solo
quando il suo contenuto viene utilizzato tramite JavaScript. Proviamo a
spiegarci con un esempio. Supponiamo di avere il seguente markup in una
pagina HTML:
<template id="hello">
<h1>Hello, HTML.it!</h1>
</template>
Questo markup non verrà visualizzato all'interno della pagina fino a quando non verrà eseguito questo codice:
function mostraTemplate() {
const template = document.getElementById("hello");
const templateContent = template.content.cloneNode(true);
document.body.appendChild(templateContent);
}
Come possiamo vedere, la funzione mostraTemplate()
accede
all'elemento template, ne clona il contenuto tramite il metodo cloneNode()
ed appende questo contenuto al body della pagina.
L'effetto finale sarà il rendering del markup contenuto nel template. Anche
se in questo esempio specifico non sarebbe stato necessario, è buona
pratica clonare il contenuto del template prima di appenderlo al DOM.
Questo evita di cambiare la sua struttura iniziale nel caso in cui si abbia
bisogno di apportare modifiche prima di renderlo visibile.
Come possiamo sfruttare i template nell'implementazione di un Web
component? Dal momento che costruiamo il nostro componente dentro un file
JavaScript, definiamo il nostro template di base come mostrato di seguito:
const template = document.createElement('template');
template.innerHTML = `
<style>
.rating {
color: orange;
}
</style>
<div id="root">
</div>
`;
Abbiamo definito l'elemento template
contenente la definizione
dello stile ed il <div>
contenitore del nostro componente.
Naturalmente, in componenti più complessi il markup può essere più
consistente.
Il template verrà usato nel costruttore del componente come mostrato di
seguito:
class MyRating extends HTMLElement {
constructor() {
this._value = 0;
this.attachShadow({
mode: "open"
});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
...
}
Quindi, abbiamo agganciato allo shadow DOM del componente il markup del template per inizializzare la sua struttura. A questo punto il resto del codice si adeguerà di conseguenza come possiamo osservare in questo brano di codice:
class MyRating extends HTMLElement {
...
connectedCallback() {
this.createStarList();
}
createStarList() {
let div = this.shadowRoot.getElementById("root");
let star;
div.innerHTML = "";
for (let i = 1; i <= this.maxValue; i++) {
if (i <= this.value) {
star = this.createStar("★", i);
}
replaceStarList() {
let starNode = this.shadowRoot.getElementById("root");
if (starNode) {
this.createStarList();
}
}
}
}
...
}
Notiamo la scomparsa del metodo createComponent()
, dal momento che
il suo compito viene assolto dal template, e la modifica dei metodi createStarList()
e replaceStartList()
che iniziano la
loro costruzione proprio a partire dall'elemento root definito
all'interno del template.
Fin qui abbiamo utilizzato i template solo per la struttura generale del
componente. Possiamo estendere il loro utilizzo anche alla parte statica
degli elementi stella. Definiamo quindi il nuovo template nel seguente
modo:
const starTemplate = document.createElement('template');
starTemplate.innerHTML = `
<span class="rating"></span>
`;
A questo punto, ridefiniamo il metodo createStar()
come mostrato di seguito:
createStar(starCode, index) {
const starNode = starTemplate.content.cloneNode(true);
const span = starNode.querySelector("span");
span.addEventListener("click", () => {
this.setAttribute("value", index);
});
span.innerHTML = starCode;
return span;
}
Vediamo come la creazione dell'elemento parte dal markup del template e
quindi spariscono le istruzioni che creavano lo <span>
ed
assegnavano la classe CSS.
Oltre a semplificare la costruzione della struttura di un Web component, i
template ci offrono la possibilità di consentire un ulteriore livello di
personalizzazione dei componenti. Infatti, insieme ai template possiamo
utilizzare gli elementi <slot>
per consentire all'utente del
nostro componente di aggiungere del markup personalizzato. Supponiamo, ad
esempio, di voler aggiungere un'etichetta al nostro componente. Sarà
sufficiente modificare il markup del template del nostro componente come
nel seguente esempio:
template.innerHTML = `
<style>
.rating {
color: orange;
}
</style>
<i>Dai il tuo voto</i>
<div id="root">
</div>
`;
Abbiamo semplicemente aggiunto il testo della nostra etichetta prima del <div>
contenente la struttura del nostro componente. Ma se
volessimo consentire all'utente di inserire il proprio markup? Non stiamo
parlando di personalizzare il solo testo, per il quale potremmo aggiungere
una proprietà, ma proprio di personalizzare il markup che rappresenta
l'etichetta.
Possiamo farlo utilizzando l'elemento <slot>.
Modifichiamo,
quindi, il markup del template come mostrato di seguito:
template.innerHTML = `
<style>
.rating {
color: orange;
}
</style>
<slot name="label"><i>Dai il tuo voto</i></slot>
<div id="root">
</div>
`;
Questa semplice modifica abilita l'utente ad inserire il suo markup personalizzato che, se specificato, andrà a sostituire quello predefinito. Il seguente è un esempio di markup personalizzato per il nostro componente:
<my-rating id="myRatingComponent" max-value="6" value="3">
<p slot="label">
Il <b>tuo giudizio</b> è importante per noi. <br/>
Comunicaci il tuo grado di soddifazione.
</p>
</my-rating>
In questo caso, l'attributo slot
con il valore label indica al componente che l'elemento specificato va a sostituire lo slot omonimo all'interno del componente stesso. L'effetto finale sarà quello mostrato dalla seguente immagine: