21 marzo 2021

Attributi e proprietà

Quando il browser carica una pagina, “legge” (in altre parole: “analizza”) l’HTML e genera gli oggetti DOM. Per i nodi elemento, i più degli attributi HTML standard diventano automaticamente proprietà degli oggetti DOM.

Ad esempio, se il tag è <body id="page">, allora l’oggetto DOM ha body.id="page".

Ma la mappatura attributo-proprietà non è uno a uno! In questo capitolo faremo attenzione a separare queste due nozioni, vedremo come lavorare con esse, quando sono la stessa cosa e quando invece sono differenti.

Proprietà del DOM

Abbiamo già visto proprietà integrate del DOM. Ce ne sono molte. Ma tecnicamente nessuna di esse ci limita, e se non sono sufficienti, possiamo aggiungerne.

I nodi del DOM sono regolari oggetti JavaScript. Possiamo modificarli.

Ad esempio, creiamo una nuova proprietà in document.body:

document.body.myData = {
  name: 'Caesar',
  title: 'Imperator'
};

alert(document.body.myData.title); // Imperator

Possiamo anche aggiungere un metodo:

document.body.sayTagName = function() {
  alert(this.tagName);
};

document.body.sayTagName(); // BODY (il valore di "this" nel metodo è document.body)

Possiamo anche modificare prototipi integrati come Element.prototype e aggiungere metodi a tutti gli elementi:

Element.prototype.sayHi = function() {
  alert(`Hello, I'm ${this.tagName}`);
};

document.documentElement.sayHi(); // Hello, I'm HTML
document.body.sayHi(); // Hello, I'm BODY

Quindi, le proprietà e i metodi del DOM si comportano come normali oggetti JavaScript:

  • Possono avere qualsiasi valore.
  • Sono sensibili alle maiuscole (scrivi elem.nodeType, non elem.NoDeTyPe).

Attributi HTML

In HTML, i tag possono avere attributi. Quando il browser analizza l’HTML per creare gli oggetti DOM corrispondenti, riconosce gli attributi standard e crea proprietà del DOM a partire da essi.

Perciò, quando un elemento ha id o un altro attributo standard, la proprietà corrispondente viene creata. Ma questo non succede se l’attributo non è standard.

Ad esempio:

<body id="test" something="non-standard">
  <script>
    alert(document.body.id); // test
    // un attributo non standard non ritorna una proprietà
    alert(document.body.something); // undefined
  </script>
</body>

Nota che un attributo standard di un elemento può essere sconosciuto ad un altro. Ad esempio, "type" è standard per <input> (HTMLInputElement), ma non per <body> (HTMLBodyElement). Gli attributi standard sono descritti nella specifica della classe corrispondente dell’elemento.

Possiamo vederlo qui:

<body id="body" type="...">
  <input id="input" type="text">
  <script>
    alert(input.type); // text
    alert(body.type); // undefined: proprietà del DOM non creata, perché non standard
  </script>
</body>

Dunque, se un attributo non è standard, non vi sarà una corrispondente proprietà del DOM. C’è un modo per accedere a questi attributi?

Certo. Tutti gli attributi sono accessibili utilizzando i seguenti metodi:

  • elem.hasAttribute(name) – controlla l’esistenza.
  • elem.getAttribute(name) – ritorna il valore.
  • elem.setAttribute(name, value) – imposta un valore.
  • elem.removeAttribute(name) – rimuove l’attributo.

Questi metodi operano con quello che è scritto in HTML.

Si possono altresì leggere tutti gli attributi utilizzando elem.attributes: una collection di oggetti che appartengono alla classe integrata Attr, con proprietà come name e value.

Segue un esempio di lettura di proprietà non standard:

<body something="non-standard">
  <script>
    alert(document.body.getAttribute('something')); // non standard
  </script>
</body>

Gli attributi HTML hanno le seguenti caratteristiche:

  • I loro nomi sono insensibili alle maiuscole (id è lo stesso di ID).
  • I loro valori sono sempre stringhe.

Ecco un esempio esaustivo di lavoro con gli attributi:

<body>
  <div id="elem" about="Elephant"></div>

  <script>
    alert( elem.getAttribute('About') ); // (1) 'Elephant', legge

    elem.setAttribute('Test', 123); // (2), scrive

    alert( elem.outerHTML ); // (3), controlla se l'attributo è nell'HTML (sì)

    for (let attr of elem.attributes) { // (4) li elenca tutti
      alert( `${attr.name} = ${attr.value}` );
    }
  </script>
</body>

Nota:

  1. getAttribute('About') – la prima lettera è maiuscola, mentre in HTML è minuscola. Non ha importanza: i nomi degli attributi sono insensibili alle maiuscole.
  2. Possiamo assegnare qualsiasi cosa a un attributo, ma diventerà una stringa. Perciò qui abbiamo "123" come valore.
  3. Tutti gli attributi, inclusi quelli che abbiamo impostato, sono visibili in outerHTML.
  4. La collection attributes è iterabile e ha tutti gli attributi dell’elemento (standard e non standard) come oggetti con proprietà name e value.

Sincronizzazione proprietà-attributo

Quando un attributo standard viene cambiato, la proprietà corrispondente viene automaticamente aggiornata, e (con qualche eccezione) vice versa.

Nell’esempio sotto id viene modificato come attributo, e possiamo vedere che anche la proprietà è cambiata. E questo anche al contrario:

<input>

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

  // attributo => proprietà
  input.setAttribute('id', 'id');
  alert(input.id); // id (aggiornato)

  // proprietà => attributo
  input.id = 'newId';
  alert(input.getAttribute('id')); // newId (aggiornato)
</script>

Ma ci sono eccezioni; ad esempio input.value si sincronizza solo da attributo a proprietà, ma non al contrario:

<input>

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

  // attributo => proprietà
  input.setAttribute('value', 'text');
  alert(input.value); // text

  // NON proprietà => attributo
  input.value = 'newValue';
  alert(input.getAttribute('value')); // text (non aggiornato!)
</script>

Nell’esempio sopra:

  • Cambiare l’attributo value aggiorna la proprietà corrispondente.
  • Ma la modifica della proprietà non cambia l’attributo.

Questa caratteristica può essere utile perché le azioni dell’utente potrebbero portare a cambi di value, e dopo questi, se vogliamo recuperare il valore “originale” dall’HTML, lo troveremo nell’attributo.

Le proprietà del DOM sono tipizzate

Le proprietà del DOM non sono sempre stringhe. Ad esempio, la proprietà input.checked (per i checkbox) è un booleano:

<input id="input" type="checkbox" checked> checkbox

<script>
  alert(input.getAttribute('checked')); // il valore dell'attributo è: stringa vuota
  alert(input.checked); // il valore della proprietà è: true
</script>

Ci sono altri esempi. L’attributo style è una stringa, ma la proprietà style non lo è.

<div id="div" style="color:red;font-size:120%">Hello</div>

<script>
  // stringa
  alert(div.getAttribute('style')); // color:red;font-size:120%

  // object
  alert(div.style); // [object CSSStyleDeclaration]
  alert(div.style.color); // red
</script>

La maggior parte delle proprietà, comunque, sono stringhe.

Raramente, anche se il tipo di una proprietà è una stringa, questa potrebbe essere diversa dall’attributo. Ad esempio, la proprietà DOM href è sempre un URL completo, anche se l’attributo contiene un URL relativo o semplicemente un #hash.

Un esempio:

<a id="a" href="#hello">link</a>
<script>
  // attributo
  alert(a.getAttribute('href')); // #hello

  // proprietà
  alert(a.href ); // URL intera nella forma http://site.com/page#hello
</script>

Se necessitiamo del valore di href o di un qualsiasi altro attributo così come si trova nell’HTML, possiamo utilizzare getAttribute.

Attributi non standard, dataset

Quando scriviamo HTML, utilizziamo molti attributi standard. E quelli non standard, personalizzati? Prima di tutto, vediamo se sono utili o meno, e per cosa.

Qualche volta gli attributi non standard sono utilizzati per passare dati personalizzati dall’HTML a JavaScript, o per “contrassegnare” elementi.

Così:

<!-- qui contrassegna div come "name" -->
<div show-info="name"></div>
<!-- e qui come age -->
<div show-info="age"></div>

<script>
  // il codice trova l'elemento con il contrassegno e mostra quel che vi è contenuto
  let user = {
    name: "Pete",
    age: 25
  };

  for(let div of document.querySelectorAll('[show-info]')) {
    // inserisci l'info corrispondente nel field
    let field = div.getAttribute('show-info');
    div.innerHTML = user[field]; // prima Pete in "name", poi 25 in "age"
  }
</script>

Possono anche essere utilizzati per stilizzare un elemento.

Ad esempio, qui per lo stato degli ordini viene utilizzato l’attributo order-state:

<style>
  /* lo stile si basa sull'attributo personalizzato "order-state" */
  .order[order-state="new"] {
    color: green;
  }

  .order[order-state="pending"] {
    color: blue;
  }

  .order[order-state="canceled"] {
    color: red;
  }
</style>

<div class="order" order-state="new">
  Un nuovo ordine.
</div>

<div class="order" order-state="pending">
  Un ordine in attesa.
</div>

<div class="order" order-state="canceled">
  Un ordine cancellato.
</div>

Perché un attributo dovrebbe essere preferibile a una classe come .order-state-new, .order-state-pending, .order-state-canceled?

Perché un attributo è più semplice da gestire. Lo stato può cambiare così:

// un po' più semplice del rimuovere/aggiungere una nuova classe
div.setAttribute('order-state', 'canceled');

Ma c’è un potenziale problema con gli attributi personalizzati. Che succederebbe se utilizzassimo un attributo non standard che in seguito verrebbe reso tale, con le sue caratteristiche? Il linguaggio HTML è vivo, cresce, e altri attributi potrebbero apparire per soddisfare le necessità degli sviluppatori.

Per evitare conflitti, abbiamo a disposizione gli attributi data-*.

Tutti gli attributi che cominciano con “data-” sono riservati all’uso dei programmatori. Sono disponibili nella proprietà dataset.

Ad esempio, se un elem ha un attributo chiamato "data-about", questo sarà disponibile come elem.dataset.about.

Così:

<body data-about="Elephants">
<script>
  alert(document.body.dataset.about); // Elephants
</script>

Attributi con più parole come data-order-state vengono camel-cased: dataset.orderState.

Un esempio di riscrittura di “order state”

<style>
  .order[data-order-state="new"] {
    color: green;
  }

  .order[data-order-state="pending"] {
    color: blue;
  }

  .order[data-order-state="canceled"] {
    color: red;
  }
</style>

<div id="order" class="order" data-order-state="new">
  Un nuovo ordine.
</div>

<script>
  // legge
  alert(order.dataset.orderState); // new

  // modifica
  order.dataset.orderState = "pending"; // (*)
</script>

Utilizzare attributi data-* è un modo valido e sicuro per passare dati personalizzati.

Nota, possiamo non solo leggere, ma anche modificare “attributi-data”. CSS aggiorna l’aspetto conseguentemente: nell’esempio sopra l’ultima linea (*) cambia il colore a blu.

Riepilogo

  • Attributi – quel che è scritto in HTML.
  • Proprietà – quel che si trova negli oggetti DOM.

Una piccola comparazione:

Proprietà Attributi
Tipo Qualsiasi valore, le proprietà standard hanno i tipi descritti nella specifica Una stringa
Nome Nome è sensibile alle maiuscole Nome non è sensibile alle maiuscole

I metodi per lavorare con gli attributi sono:

  • elem.hasAttribute(name) – controlla l’esistenza.
  • elem.getAttribute(name) – ritorna il valore.
  • elem.setAttribute(name, value) – imposta un valore.
  • elem.removeAttribute(name) – rimuove l’attributo.
  • elem.attributes una collection di tutti gli attributi.

Nella maggior parte delle situazioni è preferibile utilizzare le proprietà del DOM. Dovremmo riferirci agli attributi solo quando le proprietà non sono sufficienti, quando abbiamo bisogno dell’attributo esatto; ad esempio:

  • Abbiamo bisogno di un attributo non standard. Ma se comincia con data-, allora dovremmo utilizzare dataset.
  • Dobbiamo leggere un valore così “come scritto” in HTML. Il valore della corrispondente proprietà DOM potrebbe essere diverso; ad esempio la proprietà href è sempre un URL completo, e vorremmo il valore “originale”.

Esercizi

importanza: 5

Scrivi il codice per selezionare e leggere il valore degli elementi del documento con l’attributo data-widget-name.

<!DOCTYPE html>
<html>
<body>

  <div data-widget-name="menu">Choose the genre</div>

  <script>
    /* il tuo codice */
  </script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>

  <div data-widget-name="menu">Choose the genre</div>

  <script>
    // trovalo
    let elem = document.querySelector('[data-widget-name]');

    // leggi il valore
    alert(elem.dataset.widgetName);
    // oppure
    alert(elem.getAttribute('data-widget-name'));
  </script>
</body>
</html>
importanza: 3

Rendi tutti i link esterni arancioni modificandone la proprietà style.

Un link è esterno se:

  • Il suo href ha ://
  • Ma non comincia con http://internal.com.

Esempio:

<a name="list">the list</a>
<ul>
  <li><a href="http://google.com">http://google.com</a></li>
  <li><a href="/tutorial">/tutorial.html</a></li>
  <li><a href="local/path">local/path</a></li>
  <li><a href="ftp://ftp.com/my.zip">ftp://ftp.com/my.zip</a></li>
  <li><a href="http://nodejs.org">http://nodejs.org</a></li>
  <li><a href="http://internal.com/test">http://internal.com/test</a></li>
</ul>

<script>
  // imposta lo stile per un singolo link
  let link = document.querySelector('a');
  link.style.color = 'orange';
</script>

Il risultato dovrebbe essere:

Apri una sandbox per l'esercizio.

Prima di tutto, dobbiamo trovare tutte le referenze esterne.

Ci sono due modi.

Il primo è quello di trovare tutti i link utilizzando document.querySelectorAll('a') e poi filtrare quelli che ci servono:

let links = document.querySelectorAll('a');

for (let link of links) {
  let href = link.getAttribute('href');
  if (!href) continue; // nessun attributo

  if (!href.includes('://')) continue; // nessun protocollo

  if (href.startsWith('http://internal.com')) continue; // interno

  link.style.color = 'orange';
}

Nota: utilizziamo link.getAttribute('href'), non link.href, perché abbiamo bisogno del valore dall’HTML.

…Un’altra, più semplice alternativa sarebbe aggiungere dei controlli ai selettori CSS:

// cerca tutti i link che hanno :// in href
// ma href non inizia con http://internal.com
let selector = 'a[href*="://"]:not([href^="http://internal.com"])';
let links = document.querySelectorAll(selector);

links.forEach(link => link.style.color = 'orange');

Apri la soluzione in una sandbox.

Mappa del tutorial