Il web component che abbiamo realizzato fino a qui risponde alle nostre esigenze:
- visualizza tramite un numero di stelle colorate il valore assegnato staticamente nel markup
- visualizza il medesimo valore tramite assegnamento via JavaScript
- modifica il valore corrente e la relativa visualizzazione in seguito all'interazione con l'utente
Abbiamo quindi un componente a tutti gli effetti riusabile in pagine diverse e in progetti diversi.
Tuttavia, la sua struttura interna non è del tutto indifferente al contesto esterno. In altre parole, il comportamento del nostro componente segue le nostre aspettative all'interno di una pagina HTML semplice come quella usata nel nostro esempio, ma chi ci garantisce che non ci siano condizioni tali per cui il contesto della pagina non possa influire sull'aspetto o sul comportamento del nostro componente?
Proviamo a chiarire questo concetto con un esempio. Supponiamo di inserire il nostro componente all'interno di una pagina HTML che ha già una definizione di stile come la seguente:
<style>
span {
background-color: red;
}
</style>
Questa regola CSS assegna il rosso come colore di sfondo a ciascun elemento <span>
presente nella pagina. Se inseriamo il nostro componente in questa pagina, otterremo un effetto grafico analogo a quello mostrato dalla seguente figura:
Dal momento che il nostro componente utilizza al suo interno l'elemento <span>
, anch'esso verrà influenzato dalla definizione della regola CSS. Probabilmente non è l'effetto che vorremmo. Quello che ci aspetteremmo è che l'implementazione interna del nostro componente fosse immune da influenze esterne.
Naturalmente, il problema non si limita soltanto all'applicazione degli stili CSS. La struttura interna del nostro componente potrebbe essere intaccata anche da codice JavaScript come quello mostrato di seguito:
window.onload = function() {
let ratings = document.querySelectorAll("span");
ratings.forEach(element => {
element.innerHTML = "Hacked!"
});
Questo script sostituisce il markup interno di tutti gli elementi <span>
contenuti nella pagina con il testo Hacked!, generando il seguente effetto sul nostro componente:
Naturalmente quelli mostrati sono casi limite, ma ci servono per mostrare come la struttura interna del componente così come l'abbiamo realizzato non è indifferente all'ambiente in cui viene inserito.
Come possiamo isolare la struttura interna del nostro web component in modo da avere un maggior controllo sul suo aspetto e di consentire eventuali personalizzazioni soltanto tramite attributi e proprietà? L'utilizzo dello shadow DOM è la risposta a questa domanda.
Lo shadow DOM è un insieme di API standard che supportano l'incapsulamento di stile e markup per un web component in modo tale da non subire gli effetti che abbiamo visto prima. Grazie all'uso dello shadow DOM, possiamo associare ad un componente un DOM privato, proprio come se fosse il DOM di una pagina HTML, il cui markup e stile non è influenzato dalla pagina che ospita il componente.
Iniziamo a vedere come utilizzare lo shadow DOM applicandolo al nostro componente per evitare gli effetti evidenziati prima. A questo scopo, modifichiamo il costruttore del nostro componente come mostrato di seguito:
constructor() {
super();
this._maxValue = 5;
this._value = 0;
this.attachShadow({mode: "open"});
}
Rispetto al codice precedente, notiamo l'invocazione del metodo attachShadow()
dell'elemento corrente, cioè del web component stesso. Questo metodo è disponibile per qualsiasi elemento HTML ed è il metodo che fornisce il supporto per lo shadow DOM. Essenzialmente esso genera la root per un DOM che andremo a costruire nel metodo createComponent()
e che sarà protetto dall'ambiente esterno. Per inciso, spesso lo shadow DOM è anche detto shadow tree.
Notiamo che il metodo attachShadow()
richiede che venga passato un oggetto come parametro per specificare la modalità di creazione del DOM. Nel nostro esempio abbiamo impostato il valore "open" come modalità del DOM. Ciò vuol dire che il nostro DOM risulterà accessibile dall'esterno via JavaScript. Il valore alternativo è "closed", che impedisce completamente l'accesso alla struttura dello shadow DOM dall'esterno.
Dopo aver fatto questa modifica al costruttore, adeguiamo il metodo createComponent()
come mostrato dal seguente codice:
createComponent() {
let style = document.createElement("style");
style.appendChild(document.createTextNode(styleRules));
this.shadowRoot.appendChild(style);
let starList = this.createStarList();
this.shadowRoot.appendChild(starList);
}
La differenza che notiamo rispetto alla versione precedente del codice è l'accesso alla proprietà shadowRoot
per l'aggancio degli elementi del DOM creati, invece dell'aggancio diretto a this
. Questa proprietà espone lo shadow DOM del componente creato nel costruttore dal metodo attachShadow()
, in modo tale che sia accessibile via JavaScript.
Naturalmente occorrerà adeguare anche il resto del codice, facendo in modo che utilizzi la shadowRoot
per accedere agli elementi interni del componente invece che direttamente all'oggetto this
. È questo il caso del metodo replaceStarList()
, che andremo a modificare come segue:
replaceStarList() {
let starNode = this.shadowRoot.children[1];
if (starNode) {
let starList = this.createStarList();
starNode.remove();
this.shadowRoot.appendChild(starList);
}
}
A questo punto il nostro web component risulterà protetto da regole CSS o da codice JavaScript esterni che operano su elementi generici.