30 aprile 2022

Le proprietà del nodo: tipo, tag e contenuto

Diamo uno sguardo più approfondito ai nodi del DOM.

In questo capitolo vedremo meglio cosa sono e impareremo le loro proprietà più utilizzate.

Le classi dei nodi del DOM

Nodi del DOM differenti possono avere proprietà differenti. Ad esempio, un nodo elemento corrispondente ad un tag <a> avrà proprietà tipiche dei link ed un nodo corrispondente al tag <input> avrà proprietà tipiche dei campi di testo e così via. I nodi di testo sono differenti dai nodi elemento, tuttavia condividono alcune proprietà e metodi comuni a tutti, perché tutte le classi dei nodi del DOM costituiscono un’unica gerarchia.

Ogni nodo del DOM appartiene alla corrispondente classe nativa.

La classe base della gerarchia è EventTarget, che è ereditata dalla classe Node da cui ereditano le altre classi corrispondenti ai nodi del DOM.

Qui lo schema, le spiegazioni a seguire:

Le classi sono:

  • EventTarget – è la classe radice (root class) “astratta”. Gli oggetti di questa classe non vengono mai creati. Serve solo come base, in questo modo tutti i nodi del DOM supportano i cosiddetti “eventi” che studieremo successivamente.
  • Node – anche questa è una classe “astratta” che serve da base per i nodi del DOM. Fornisce le funzionalità principali della struttura gerarchica: parentNode, nextSibling, childNodes e così via (si tratta di getter). Dalla classe Node non vengono mai creati oggetti, tuttavia da questa ereditano classi corrispondenti a nodi concreti, nella fattispecie: Text per i nodi di testo, Element per i nodi elemento e quelli meno ricorrenti come Comment per i nodi commento.
  • Element – è la classe base per gli elementi del DOM. Fornisce le funzionalità di navigazione tra elementi come nextElementSibling, children ed i metodi di ricerca come getElementsByTagName, querySelector. Un browser non supporta solo HTML, ma anche XML e SVG. La classe Element serve da base per le classi più specifiche: SVGElement, XMLElement e HTMLElement.
  • HTMLElement – è, infine, la classe base per tutti gli elementi HTML. Essa è ereditata da elementi HTML concreti:
    • HTMLInputElement – la classe per gli elementi <input>,
    • HTMLBodyElement – la classe per gli elementi <body>,
    • HTMLAnchorElement – la classe per gli elementi <a>,
    • …e così via, ogni tag ha una propria classe che espone proprietà e metodi specifici.

In definitiva, la lista completa delle proprietà e dei metodi di un nodo è il risultato dell’ereditarietà.

Consideriamo, ad esempio, l’oggetto DOM per un elemento <input> che appartiene alla classe HTMLInputElement.

Esso riceve proprietà e metodi per effetto della sovrapposizione di (elencate in ordine di ereditarietà):

  • HTMLInputElement – questa classe fornisce le proprietà specifiche per un campo di testo,
  • HTMLElement – espone i metodi (e i getter/setter) comuni agli elementi HTML,
  • Element – fornisce i metodi generici propri di un elemento,
  • Node – fornisce i metodi generici propri di un nodo DOM,
  • EventTarget – consente il supporto agli eventi (che tratteremo in seguito),
  • …e, infine, esso eredita da Object, quindi saranno disponibili anche i metodi di un oggetto semplice come hasOwnProperty.

Per conoscere il nome della classe di un nodo DOM, ricordiamoci che un oggetto ha solitamente la proprietà constructor. Questa contiene un riferimento al costruttore della classe e constructor.name indica il suo nome:

alert( document.body.constructor.name ); // HTMLBodyElement

…Oppure possiamo semplicemente eseguire il metodo toString:

alert( document.body ); // [object HTMLBodyElement]

Possiamo inoltre usare instanceof per verificare l’ereditarietà:

alert( document.body instanceof HTMLBodyElement ); // true
alert( document.body instanceof HTMLElement ); // true
alert( document.body instanceof Element ); // true
alert( document.body instanceof Node ); // true
alert( document.body instanceof EventTarget ); // true

Come possiamo notare i nodi DOM sono oggetti JavaScript regolari ed usano classi basate sui prototipi per l’ereditarietà.

Questo è facilmente osservabile esaminando un elemento in un browser con console.dir(elem). Nella console potremo vedere HTMLElement.prototype, Element.prototype e così via.

console.dir(elem) versus console.log(elem)

La maggior parte dei browser supportano due comandi nei loro strumenti per sviluppatori: console.log e console.dir che mostrano in console i loro argomenti. Per quanto riguarda gli oggetti JavaScript, solitamente questi comandi funzionano allo stesso modo.

Ma per gli elementi DOM sono differenti:

  • console.log(elem) mostra l’alberatura DOM dell’elemento.
  • console.dir(elem) mostra l’elemento come oggetto DOM, ottimo per esplorarne le proprietà.

Provatelo con document.body.

L’IDL della specifica

Nella specifica, le classi DOM non sono descritte con JavaScript, ma con uno speciale Interface description language (IDL), che di solito è facile da comprendere.

Nell’IDL tutte le proprietà sono precedute dai rispettivi tipi. Per esempio DOMString, boolean e così via.

Eccone un estratto commentato:

// Definisce HTMLInputElement
// I due punti ":" significano che HTMLInputElement eredita da HTMLElement
interface HTMLInputElement: HTMLElement {
  // seguono le proprietà ed i metodi degli elementi <input>

  // "DOMString" significa che il valore di una proprietà è una stringa
  attribute DOMString accept;
  attribute DOMString alt;
  attribute DOMString autocomplete;
  attribute DOMString value;

  // proprietà con valore booleano (true/false)
  attribute boolean autofocus;
  ...
  // ora un metodo: "void" per indicare che il metodo non restituisce alcun valore
  void select();
  ...
}

La proprietà “nodeType”

La proprietà nodeType offre un altro modo “vecchio stile” per ricavare il “tipo” di un nodo DOM.

Ha un valore numerico:

  • elem.nodeType == 1 per i nodi elemento,
  • elem.nodeType == 3 per i nodi testo,
  • elem.nodeType == 9 per l’oggetto documento,
  • c’è qualche altro valore nella specifica.

Per esempio:

<body>
  <script>
  let elem = document.body;

  // esaminiamo di cosa si tratta
  alert(elem.nodeType); // 1 => nodo elemento

  // e il primo nodo figlio è...
  alert(elem.firstChild.nodeType); // 3 => nodo testo

  // per l'oggetto documento il tipo è 9
  alert( document.nodeType ); // 9
  </script>
</body>

Nel codice moderno possiamo usare instanceof e altri test basati sulle classi per ottenere il tipo di nodo, ma, talvolta, può risultare più immediato l’uso di nodeType. La proprietà nodeType è in sola lettura, non possiamo modificarla.

Tag: nodeName e tagName

Dato un nodo DOM, possiamo leggerne il tag tramite le proprietà nodeName o tagName:

Per esempio:

alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY

Esiste una differenza tra tagName e nodeName?

Certamente, i nomi stessi delle proprietà suggeriscono la sottile differenza.

  • La proprietà tagName esiste solo per i nodi Element.
  • La proprietà nodeName è definita per ogni Node:
    • per i nodi elemento ha lo stesso valore di tagName.
    • per gli altri tipi di nodo (testo, commento, ecc.) contiene una stringa che indica il tipo di nodo.

In altre parole, tagName è supportata solo dai nodi elemento (poiché ha origine dalla classe Element), mentre nodeName riesce a dare un’indicazione sugli altri tipi di nodo.

Per esempio paragoniamo tagName e nodeName per document e per un commento:

<body><!-- comment -->

  <script>
    // per un commento
    alert( document.body.firstChild.tagName ); // undefined (non si tratta di un elemento)
    alert( document.body.firstChild.nodeName ); // #comment

    // per il documento
    alert( document.tagName ); // undefined (non si tratta di un elemento)
    alert( document.nodeName ); // #document
  </script>
</body>

Se abbiamo a che fare solo con elementi allora possiamo usare senza distinzione tagName e nodeName

Il nome del tag è sempre in maiuscolo tranne che per l’XML

Il browser ha due modalità di elaborazione dei documenti: HTML e XML. Solitamente per le pagine web usa la modalità HTML. La modalità XML è abilitata quando il browser riceve un documento XML con l’intestazione Content-Type: application/xml+xhtml.

In modalità HTML tagName/nodeName è sempre maiuscola: restituisce BODY sia per <body> sia per <BoDy>.

In modalità XML il case viene mantenuto “così com’è”. Ai giorni nostri la modalità XML è usata raramente.

innerHTML: i contenuti

La proprietà innerHTML consente di ottenere una stringa contenente l’HTML dentro l’elemento.

Possiamo anche modificarla e pertanto è uno dei più potenti strumenti per cambiare l’HTML di un elemento della pagina.

L’esempio mostra il contenuto di document.body e poi lo rimpiazza completamente:

<body>
  <p>A paragraph</p>
  <div>A div</div>

  <script>
    alert( document.body.innerHTML ); // legge il contenuto corrente
    document.body.innerHTML = 'The new BODY!'; // lo rimpiazza
  </script>

</body>

Se proviamo a inserire HTML non valido, il browser correggerà i nostri errori:

<body>

  <script>
    document.body.innerHTML = '<b>test'; // tag non chiuso correttamente
    alert( document.body.innerHTML ); // <b>test</b> (corretto)
  </script>

</body>
I tag <script> non vengono eseguiti

Se innerHTML inserisce un tag <script> nel documento – esso diviene parte dell’HTML ma non viene eseguito.

Attenzione: “innerHTML+=” esegue una sovrascrittura completa

Possiamo aggiungere HTML a un elemento usando elem.innerHTML+="more html".

In questo modo:

chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "How goes?";

Tuttavia dovremmo stare molto attenti nel fare un’operazione del genere, perché non stiamo facendo una semplice aggiunta ma una sovrascrittura completa.

Tecnicamente queste due righe sono equivalenti:

elem.innerHTML += "...";
// è un modo più rapido di scrivere:
elem.innerHTML = elem.innerHTML + "..."

In altre parole, innerHTML+= fa questo:

  1. Rimuove il contenuto precedente.
  2. Il nuovo valore di innerHTML (una concatenazione del vecchio e del nuovo) è inserito al suo posto.

Poiché il contenuto viene “azzerato” e riscritto da zero, tutte le immagini e le altre risorse verranno ricaricate.

Nell’esempio chatDiv sopra, la linea chatDiv.innerHTML+="How goes?" ricrea il contenuto HTML e ricarica smile.gif (speriamo sia in cache). Se chatDiv ha molto altro testo e immagini il tempo di ricaricamento potrebbe diventare chiaramente percepibile.

Ci sono anche altri effetti collaterali. Per esempio, se il testo esistente era stato selezionato con il mouse, la maggior parte dei browser rimuoveranno la selezione al momento della riscrittura con innerHTML. Se un elemento <input> conteneva un testo digitato dal visitatore il testo sarà rimosso, e altri casi simili.

Fortunatamente ci sono altri modi di aggiungere HTML oltre che con innerHTML, presto li studieremo.

outerHTML: l’HTML completo di un elemento

La proprietà outerHTML contiene tutto l’HTML di un elemento. In pratica equivale a innerHTML più l’elemento stesso.

Di seguito un esempio:

<div id="elem">Hello <b>World</b></div>

<script>
  alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>
</script>

Attenzione: diversamente da innerHTML, la scrittura in outerHTML non cambia l’elemento ma lo sostituisce nel DOM.

Proprio così, sembra strano, e lo è. Ecco perché ne parliamo subito con una nota a parte. Prestate attenzione.

Considerate l’esempio:

<div>Hello, world!</div>

<script>
  let div = document.querySelector('div');

  // sostituisce div.outerHTML con <p>...</p>
  div.outerHTML = '<p>A new element</p>'; // (*)

  // Wow! 'div' non è cambiato!
  alert(div.outerHTML); // <div>Hello, world!</div> (**)
</script>

Sembra davvero strano, vero?

Nella linea (*) sostituiamo div con <p>A new element</p>. Nel documento (il DOM) possiamo osservare che il nuovo contenuto ha preso il posto di <div>. Tuttavia, come possiamo notare nella linea (**), il precedente valore della variabile div non è cambiato!

L’assegnazione con outerHTML non cambia l’elemento (cioè l’oggetto a cui fa riferimento, in questo caso, la variabile ‘div’), però lo rimuove dal DOM e inserisce il nuovo HTML al suo posto.

Ricapitolando ciò che è successo in div.outerHTML=... è:

  • div è stato rimosso dal documento.
  • Un pezzo di HTML differente <p>A new element</p> è stato inserito al suo posto.
  • div mantiene ancora il suo valore precedente. L’HTML inserito in seguito non è stato memorizzato in alcuna variabile.

È molto semplice commettere un errore a questo punto: modificare div.outerHTML e procedere con div come se avesse recepito il nuovo contenuto. Ma questo non avviene. Tale convinzione è corretta per innerHTML, ma non per outerHTML.

Possiamo scrivere tramite elem.outerHTML, ma dovremmo tenere bene presente che non cambia l’elemento (‘elem’) su cui stiamo scrivendo, sostituisce invece il nuovo HTML al suo posto. Per avere un riferimento valido al nuovo elemento dobbiamo interrogare nuovamente il DOM.

nodeValue/data: il contenuto testuale del nodo

La proprietà innerHTML è valida soltanto per i nodi elemento.

Gli altri tipi di nodo, come i nodi di testo, hanno il loro corrispettivo: le proprietà nodeValue e data. Nell’uso pratico questi due si comportano quasi alla stessa maniera, ci sono solo piccole differenze nella specifica. Useremo perciò data dal momento che è più breve.

Ecco un esempio di lettura del contenuto di un nodo di testo e di un commento:

<body>
  Hello
  <!-- Comment -->
  <script>
    let text = document.body.firstChild;
    alert(text.data); // Hello

    let comment = text.nextSibling;
    alert(comment.data); // Comment
  </script>
</body>

Per i nodi di testo possiamo ipotizzare un motivo per leggerne o modificarne il contenuto testuale, ma perché i commenti?

Talvolta gli sviluppatori incorporano informazioni o istruzioni per i template nei commenti all’interno dell’HTML, in questo modo:

<!-- if isAdmin -->
  <div>Welcome, Admin!</div>
<!-- /if -->

…così JavaScript può leggerle tramite la proprietà data ed elaborare le istruzioni contenute.

textContent: solo il testo

La proprietà textContent fornisce l’accesso al testo dentro l’elemento: solo il testo, al netto di tutti i <tag>.

Per esempio:

<div id="news">
  <h1>Headline!</h1>
  <p>Martians attack people!</p>
</div>

<script>
  // Headline! Martians attack people!
  alert(news.textContent);
</script>

Come possiamo notare viene restituito solo il testo, come se tutti i <tag> fossero stati eliminati, ma il testo in essi fosse rimasto.

Leggere il testo in questa maniera è un’esigenza rara nell’uso pratico.

È molto più utile scrivere con textContent, perché consente di inserire testo “in sicurezza”.

Supponiamo di avere una stringa arbitraria, ad esempio inserita da un utente, e di volerla mostrare.

  • Con innerHTML la inseriremo “come HTML”, compresi tutti i tag HTML.
  • Con textContent la inseriremo “come testo”, tutti i simboli sono trattati letteralmente.

Paragoniamo le due opzioni:

<div id="elem1"></div>
<div id="elem2"></div>

<script>
  let name = prompt("What's your name?", "<b>Winnie-the-Pooh!</b>");

  elem1.innerHTML = name;
  elem2.textContent = name;
</script>
  1. Il primo <div> riceve il nome “come HTML”: tutti i tag diventano tali, perciò vedremo il nome in grassetto.
  2. Il secondo <div> riceve il nome “come testo”, perciò vedremo letteralmente <b>Winnie-the-Pooh!</b>.

Nella maggior parte dei casi da un utente ci aspettiamo testo e desideriamo gestirlo in quanto tale. Non vogliamo codice HTML inatteso sul nostro sito. Un’assegnazione con textContent fa esattamente questo.

La proprietà “hidden”

L’attributo “hidden” e la corrispettiva proprietà del DOM specificano se l’elemento debba essere visibile o meno.

Possiamo agire da codice HTML o da JavaScript in questo modo:

<div>Both divs below are hidden</div>

<div hidden>With the attribute "hidden"</div>

<div id="elem">JavaScript assigned the property "hidden"</div>

<script>
  elem.hidden = true;
</script>

Tecnicamente, hidden funziona alla stessa maniera di style="display:none" ma è più breve da scrivere.

Ecco come ottenere un elemento lampeggiante:

<div id="elem">A blinking element</div>

<script>
  setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>

Altre proprietà

Gli elementi DOM hanno inoltre proprietà aggiuntive, in particolare quelle che dipendono dalla classe:

  • value – il valore di <input>, <select> e <textarea> (HTMLInputElement, HTMLSelectElement…).
  • href – il valore dell’attributo “href” di <a href="..."> (HTMLAnchorElement).
  • id – il valore dell’attributo “id” per tutti gli elementi (HTMLElement).
  • …e molte altre…

Per esempio:

<input type="text" id="elem" value="value">

<script>
  alert(elem.type); // "text"
  alert(elem.id); // "elem"
  alert(elem.value); // value
</script>

La maggior parte degli attributi HTML standard ha la corrispondente proprietà DOM e possiamo accedervi in questo modo.

Se desideriamo conoscere la lista completa delle proprietà supportate per una classe precisa, le possiamo trovare nella specifica. Per esempio la classe HTMLInputElement è documentata su https://html.spec.whatwg.org/#htmlinputelement.

In alternativa, se vogliamo ricavarle rapidamente o siamo interessati ad una concreta implementazione del browser – possiamo sempre esaminare l’elemento con console.dir(elem) e leggerne le proprietà o, ancora, esplorare le “Proprietà DOM” nel tab Elementi degli strumenti per sviluppatori del browser.

Riepilogo

Ciascun nodo del DOM appartiene ad una determinata classe. Le classi costituiscono una gerarchia. L’elenco completo delle proprietà e dei metodi è il risultato dell’ereditarietà.

Le principali proprietà di un nodo DOM sono:

nodeType
Possiamo utilizzarla per sapere se si tratta di un nodo di testo o di un nodo elemento. Ha un valore numerico: 1 per gli elementi, 3 per i nodi di testo e pochi altri valori per gli altri tipi di nodo. La proprietà è in sola lettura.
nodeName/tagName
Per gli elementi indica il nome del tag (in lettere maiuscole a meno che il browser non sia in modalità XML). Per tutti gli altri nodi nodeName contiene una stringa descrittiva. La proprietà è in sola lettura.
innerHTML
Il contenuto HTML dell’elemento. Può essere modificato.
outerHTML
L’HTML completo dell’elemento. Un’operazione di scrittura in elem.outerHTML non modifica elem ma viene sostituito nel documento con il nuovo HTML.
nodeValue/data
Il contenuto dei nodi che non sono elementi (testi, commenti). Le due proprietà sono quasi del tutto equiparabili, solitamente usiamo data. Può essere modificata.
textContent
Il testo dentro un elemento: l’HTML al netto di tutti i <tag>. Scrivere testo dentro un elemento con questa proprietà fa sì che tutti i caratteri speciali ed i tag siano resi esattamente come testo. Può trattare il testo digitato da un utente in modo sicuro prevenendo gli inserimenti di HTML indesiderato.
hidden
Quando è impostata a true, è equivalente alla dichiarazione CSS display:none.

I nodi del DOM hanno inoltre altre proprietà in base alla loro classe di appartenenza. Per esempio gli elementi <input> (HTMLInputElement) supportano value, type, mentre gli elementi <a> (HTMLAnchorElement) supportano href etc. La maggior parte degli attributi HTML standard hanno una proprietà DOM corrispondente.

Ad ogni modo, come vedremo nel prossimo capitolo, gli attributi HTML e le proprietà del DOM non sono sempre corrispondenti.

Esercizi

Abbiamo un alberatura HTML strutturata come un elenco di ul/li annidati.

Scrivete il codice che per ogni elemento <li> mostri:

  1. Qual è il testo al suo interno (senza considerare il testo di eventuali sottoelementi).
  2. Il numero degli elementi <li> annidati – tutti i discendenti, considerando tutti i livelli di annidamento.

Demo in una nuova finesta

Apri una sandbox per l'esercizio.

Effettuiamo un ciclo iterativo sugli elementi <li>:

for (let li of document.querySelectorAll('li')) {
  ...
}

Per ciascuna iterazione abbiamo bisogno di ricavare il testo all’interno di ogni li.

Possiamo leggere il testo dal primo nodo figlio di li, che è un nodo di testo:

for (let li of document.querySelectorAll('li')) {
  let title = li.firstChild.data;

  // title è il testo nel <li> prima di qualsiasi altro nodo
}

A questo punto possiamo ricavare il numero dei discendenti con li.getElementsByTagName('li').length.

Apri la soluzione in una sandbox.

Cosa mostrerà lo script?

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>

C’è un tranello in questo esercizio.

Al momento dell’esecuzione di <script> l’ultimo nodo del DOM è esattamente <script>, dal momento che il browser non ha ancora elaborato il resto della pagina.

Pertanto il risultato è 1 (nodo elemento).

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>

Cosa mostrerà questo codice?

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // cosa c'è qui?
</script>

Risposta: BODY.

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // BODY
</script>

Vediamo cosa succede passo dopo passo:

  1. Il contenuto di <body> è rimpiazzato con il commento. Il commento è <!--BODY-->, poiché body.tagName == "BODY". Abbiamo detto che, tagName è sempre maiuscolo in modalità HTML.
  2. Il commento è ora l’unico nodo figlio, perciò è il risultato di body.firstChild.
  3. La proprietà data del commento è il suo contenuto (ovvero ciò che è dentro i tag di apertura e chiusura <!--...-->): "BODY".

A quale classe appartiene document?

Qual è il suo posto nella gerarchia DOM?

Eredita da Node, da Element o forse da HTMLElement?

Possiamo visualizzare a quale classe appartiene esaminandola in questo modo:

alert(document); // [object HTMLDocument]

Oppure:

alert(document.constructor.name); // HTMLDocument

Quindi document è un’istanza della classe HTMLDocument.

Qual è il suo posto nella gerarchia DOM?

Certo, potremmo sfogliare la specifica, ma sarebbe più veloce scoprirlo manualmente.

Attraversiamo la catena dei prototipi tramite __proto__.

Come sappiamo i metodi di una classe sono nel prototype del costruttore. Per esempio HTMLDocument.prototype ha i metodi per i documenti.

C’è inoltre un riferimento al costruttore all’interno di prototype:

alert(HTMLDocument.prototype.constructor === HTMLDocument); // true

Per ricavare la stringa con il nome della classe possiamo usare constructor.name. Facciamolo per l’intera catena prototipale document fino alla classeNode:

alert(HTMLDocument.prototype.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.__proto__.constructor.name); // Document
alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node

Questa è la gerachia.

Potremmo anche esaminare l’oggetto usando console.dir(document) e visualizzare gli stessi nomi aprendo __proto__. La console li ricava internamente da constructor.

Mappa del tutorial