13 marzo 2021

Ricerca: getElement*, querySelector*

Le proprietà di navigazione del DOM funzionano bene per gli elementi vicini. E quando non lo sono? Come possiamo ottenere un elemento arbitrario della pagina?

Ci sono altri metodi di ricerca per questo.

document.getElementById o semplicemente id

Se un elemento ha un attributo id possiamo trovarlo, ovunque esso sia, utilizzando il metodo document.getElementById(id)

Ad esempio:

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // trova l'elemento
  let elem = document.getElementById('elem');

  // rendi il suo background rosso
  elem.style.background = 'red';
</script>

Inoltre, c’è una variabile globale chiamata come l’id che fa riferimento all’elemento:

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // elem e' un riferimento all'elemento del DOM con id="elem"
  elem.style.background = 'red';

  // id="elem-content" contiene un trattino, quindi non può essere il nome di una variabile
  // ...ma possiamo accedervi tramite parentesi quadre: window['elem-content']
</script>

…Questo a meno di dichiarare una variabile JavaScript con lo stesso nome: quest’ultima avrebbe la precedenza:

<div id="elem"></div>

<script>
  let elem = 5; // ora elem è 5, non un riferimento a <div id="elem">

  alert(elem); // 5
</script>
Non utilizzate le variabili-id globali per accedere agli elementi

Questo comportamento è descritto nella specifica, quindi è quasi uno standard, ma è supportato principalmente per una questione di compatibilità.

Il browser cerca di aiutarci mischiando i namespaces di JS e del DOM. Questo va bene per un semplice script, ma generalmente non è una buona cosa. Potrebbero esserci conflitti tra i nomi. Inoltre, quando uno legge il codice JS e non ha sott’occhio quello HTML non è ovvia la provenienza di una variabile.

Qui nel tutorial, per brevità e quando è ovvia la provenienza dell’elemento, utilizziamo id come diretto riferimento ad esso.

Normalmente document.getElementById è il metodo da preferire.

L’id deve essere unico

Un id deve essere unico. Nel documento vi può essere un solo elemento con uno specifico id.

Se ci sono più elementi con lo stesso id il comportamento dei metodi che lo utilizzano diventa imprevedibile, ad esempio document.getElementById potrebbe ritornare un elemento casuale tra questi. Mantenete gli id unici.

Solo document.getElementById, non anyElem.getElementById

Il metodo getElementById può essere chiamato solo sull’oggetto document, cerca l’id in tutto il documento.

querySelectorAll

Tra i metodi più versatili, elem.querySelectorAll(css) ritorna tutti gli elementi contenuti in elem che combaciano con il selettore CSS specificato.

Qui cerchiamo tutti i <li> che sono gli ultimi figli:

<ul>
  <li>The</li>
  <li>test</li>
</ul>
<ul>
  <li>has</li>
  <li>passed</li>
</ul>
<script>
  let elements = document.querySelectorAll('ul > li:last-child');

  for (let elem of elements) {
    alert(elem.innerHTML); // "test", "passed"
  }
</script>

Questo metodo è molto potente poiché può utilizzare qualsiasi selettore CSS.

Può utilizzare anche pseudo-classi

Le pseudo-classi come :hover e :active sono altresì supportate. Ad esempio, document.querySelectorAll(':hover') ritornerà una collection con gli elementi che attualmente si trovano sotto al puntatore del mouse (seguendo l’ordine di annidamento, dal più esterno <html> all’elemento più annidato).

querySelector

La chiamata a elem.querySelector(css) ritorna il primo elemento che combacia con il selettore CSS specificato.

In altre parole, il risultato è lo stesso di elem.querySelectorAll(css)[0], ma quest’ultimo cerca tutti gli elementi per poi prenderne uno, mentre elem.querySelector ne cerca solo uno. E’ più veloce e più breve da scrivere.

matches

I metodi visti finora cercavano nel DOM.

Il metodo elem.matches(css) non cerca nulla; controlla semplicemente se elem combacia con il selettore CSS specificato, e ritorna true o false.

Questo metodo è utile quando stiamo iterando sopra degli elementi (come in un array) e cerchiamo di filtrare quelli che ci interessano.

Ad esempio:

<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>

<script>
  // può essere una qualsiasi collezione piuttosto di document.body.children
  for (let elem of document.body.children) {
    if (elem.matches('a[href$="zip"]')) {
      alert("The archive reference: " + elem.href );
    }
  }
</script>

closest

Gli antenati di un elemento sono: il genitore, il genitore del genitore, il genitore di quest’ultimo e così via. Gli antenati formano la catena di genitori dall’elemento in cima.

Il metodo elem.closest(css) cerca l’antenato più vicino che combacia il selettore CSS specificato. elem stesso è incluso nella ricerca.

In altre parole, il metodo closest parte dall’elemento e controlla tutti i suoi genitori. Se uno di essi combacia con il selettore, la ricerca si interrompe e l’antenato viene ritornato.

Ad esempio:

<h1>Contents</h1>

<div class="contents">
  <ul class="book">
    <li class="chapter">Chapter 1</li>
    <li class="chapter">Chapter 1</li>
  </ul>
</div>

<script>
  let chapter = document.querySelector('.chapter'); // LI

  alert(chapter.closest('.book')); // UL
  alert(chapter.closest('.contents')); // DIV

  alert(chapter.closest('h1')); // null (perché h1 non è un antenato)
</script>

getElementsBy*

Ci sono altri metodi per cercare nodi tramite tag, classi ecc.

Oggi sono più che altro storia, essendo querySelector più potente e più breve da scrivere.

Qui li studiamo per completezza, poiché possono essere ancora trovati nei vecchi script.

  • elem.getElementsByTagName(tag) cerca gli elementi con il tag specificato e ritorna una loro collection. Il parametro tag può anche essere *, che equivale a “qualsiasi tag”
  • elem.getElementsByClassName(className) ritorna gli elementi con la data classe.
  • document.getElementsByName(name) ritorna gli elementi con l’attributo name, ovunque nel documento. Raramente usato.

Ad esempio:

// trova tutti i div del documento
let divs = document.getElementsByTagName('div');

Troviamo tutti gli input tag dentro la tabella:

<table id="table">
  <tr>
    <td>Your age:</td>

    <td>
      <label>
        <input type="radio" name="age" value="young" checked> less than 18
      </label>
      <label>
        <input type="radio" name="age" value="mature"> from 18 to 50
      </label>
      <label>
        <input type="radio" name="age" value="senior"> more than 60
      </label>
    </td>
  </tr>
</table>

<script>
  let inputs = table.getElementsByTagName('input');

  for (let input of inputs) {
    alert( input.value + ': ' + input.checked );
  }
</script>
Non dimenticate la lettera "s"

Gli sviluppatori principianti scordano qualche volta la lettera "s". Cercano di chiamare getElementByTagName invece di getElementsByTagName.

La lettera "s" manca in getElementById, perché esso ritorna un singolo elemento. Ma getElementsByTagName ritorna una collezione di elementi, quindi c’è una "s".

Ritorna una collection, non un elemento!

Un altro errore diffuso tra principianti è quello di scrivere:

// non funziona
document.getElementsByTagName('input').value = 5;

Non funziona perché prende una collection di input e le assegna un valore, invece che assegnare il valore agli elementi contenuti.

Dovremmo iterare sopra la collection, oppure selezionare un elemento tramite il suo indice e quindi eseguire l’assegnazione, così:

// dovrebbe funzionare (se c'è un input)
document.getElementsByTagName('input')[0].value = 5;

Cerchiamo gli elementi con .article:

<form name="my-form">
  <div class="article">Article</div>
  <div class="long article">Long article</div>
</form>

<script>
  // cerca tramite il nome dell'attributo
  let form = document.getElementsByName('my-form')[0];

  // trova tramite la classe contenuta in form
  let articles = form.getElementsByClassName('article');
  alert(articles.length); // 2, trovati due elementi con la classe "article"
</script>

Collections vive

Tutti i metodi "getElementsBy*" ritornano una collection viva. Questa collection rispecchia sempre lo stato corrente del documento e si “auto-aggiorna” quando questo cambia.

Nell’esempio sotto ci sono due script.

  1. Il primo crea una referenza alla collezione di <div>. Inizialmente la sua length è 1.
  2. Il secondo script viene eseguito dopo di che il browser incontra un altro <div>, quindi la sua length diventa 2.
<div>First div</div>

<script>
  let divs = document.getElementsByTagName('div');
  alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
  alert(divs.length); // 2
</script>

querySelectorAll, invece, ritorna una collection statica. E’ come un array con degli elementi fissi.

Se lo utilizziamo al posto di getElementsBy*, entrambi gli script mostreranno 1:

<div>First div</div>

<script>
  let divs = document.querySelectorAll('div');
  alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
  alert(divs.length); // 1
</script>

Ora possiamo facilmente vedere la differenza. La collection statica non viene incrementata dopo l’apparizione del nuovo div nel documento.

Riepilogo

Ci sono sei metodi principali per cercare nodi nel DOM:

Metodo Cerca tramite... Può essere chiamato su un elemento? Vivo?
querySelector CSS-selector -
querySelectorAll CSS-selector -
getElementById id - -
getElementsByName name -
getElementsByTagName tag o '*'
getElementsByClassName classe

I più utilizzati sono querySelector e querySelectorAll, ma getElement(s)By* può essere utile in alcuni casi, e si trova nei vecchi script.

Inoltre:

  • C’è elem.matches(css) per controllare se elem combacia con il selettore CSS specificato.
  • C’è elem.closest(css) per cercare l’antenato più vicino che combacia con il selettore CSS specificato. Anche elem viene controllato.

Menzioniamo anche un altro metodo, che a volte può essere utile per controllare il rapporto figlio-genitore:

  • elemA.contains(elemB) ritorna true se elemB è dentro elemA (un discendente di elemA) o quando elemA==elemB.

Esercizi

importanza: 4

Abbiamo un documento con una tabella e un form.

Come trovare?…

  1. La tabella con id="age-table".
  2. Tutti gli elementi label dentro la tabella (dovrebbero essercene 3).
  3. Il primo td nella tabella (con la parola “Age”).
  4. Il form con name="search".
  5. Il primo input nel form.
  6. L’ultimo input nel form.

Apri la pagina table.html in un’altra finestra e utilizza gli strumenti del browser.

Ci sono molti modi per farlo.

Eccone alcuni:

// 1. La tabella con `id="age-table"`.
let table = document.getElementById('age-table')

// 2. Tutti gli elementi label dentro la tabella
table.getElementsByTagName('label')
// oppure
document.querySelectorAll('#age-table label')

// 3. Il primo td dentro la tabella (con la parola "Age")
table.rows[0].cells[0]
// oppure
table.getElementsByTagName('td')[0]
// oppure
table.querySelector('td')

// 4. La form con il nome "search"
// supponendo ci sia solo un elemento con name="search" nel documento
let form = document.getElementsByName('search')[0]
// oppure, form specificamente
document.querySelector('form[name="search"]')

// 5. Il primo input contenuto in form.
form.getElementsByTagName('input')[0]
// oppure
form.querySelector('input')

// 6. L'ultimo input in form
let inputs = form.querySelectorAll('input') // trova tutti gli input
inputs[inputs.length-1] // prendi l'ultimo
Mappa del tutorial