La creazione dinamica di elementi nel DOM attraverso Javascript è una pratica usuale per chi vuole sviluppare applicazioni web che richiedono di inserire nella struttura del documento informazioni on-demand (ad esempio in seguito ad una chiamata AJAX o ad un evento generato dall'utente).
In Javascript abbiamo a disposizione due modi per creare dei nuovi nodi:
- il metodo
innerHTML
, con cui appendere generiche stringhe; - i metodi
createElement()
eappendChild()
che invece creano ed appendono specificatamente elementi-nodo.
Sebbene i benchmarks confermino le migliori performances del metodo innerHTML
per un numero rilevante di elementi, soprattutto in Internet Explorer 6 e 7, questo presenta alcuni bug in documenti serviti come "application/xhtml+xml
" su alcune versioni di Safari e Firefox e può portare anche ad alcuni inconvenienti (innerHTML
riscrive completamente il contenuto di un nodo e potrebbe rimuovere elementi annidati in precedenza).
Quindi, per un numero di elementi limitato, è ragionevole pensare all'utilizzo dei metodi standard createElement()
e appendChild()
. Tuttavia il loro uso diventa ben presto ridondante in presenza di elementi annidati e/o con molti attributi: si pensi ad esempio al codice necessario per creare una semplice struttura come questa:
<p> <cite title="[cit. Sant'Agostino]"> 'Le persone viaggiano per stupirsi delle montagne, dei fiumi, delle stelle e passano accanto a sé stesse senza meravigliarsi...' </cite> </p>
Per crearla, dovremmo scrivere qualcosa come
var aPar = document.createElement('p'); var aCite = document.createElement('cite'); aCite.title = "[cit. Sant'Agostino]"; aCite.innerHTML = "Le persone viaggiano per stupirsi..."; aPar.appendChild(aCite); document.body.appendChild(aPar);
Diverse sono le problematiche che nascono da un approccio di questo tipo. È necessario:
- Dover creare nuove variabili per referenziare i nodi creati e usare delle convenzioni per evitare la sovrascrittura delle variabili stesse (ad esempio quando ci sono più nodi dello stesso tipo)
- Dover appendere correttamente i vari nodi in modo tale da essere annidati nell'ordine desiderato
- Scrivere, debuggare e mantenere una quantità notevole di codice che aumenta all'aumentare del markup
Esistono delle librerie in grado di compiere le stesse operazioni, definendo i vari nodi come sequenze annidate di strutture combimnate: è il caso della libreria Graft , con la quale potremmo definire la nostra struttura in modo simile a questo:
["p", { }, ["cite", { "title": "[cit. Sant'Agostino]", "innerHTML": "Le persone viaggiano per stupirsi..." }, ... ] ]
Che però diventa ben presto di difficile comprensione, poiché alla funzione vengono passati come argomenti sia array annidati che oggetti. Un grosso passo in avanti, in termini di leggibilità e semplicità, potrebbe essere fatto utilizzando un'unica struttura definita in notazione pure-object e creando una funzione che ricorsivamente crea ed appende nodi ed attributi così definiti.
{ "p" : { "cite": { "innerHTML": "'Le persone viaggiano ...", "title": "[cit. Sant'Agostino]" } }, "p": { "cite": { "innerHTML": "'Altra citazione", "title": "[cit. Autore]" } } }
Soluzione decisamente più elegante e concisa, ma... purtroppo non corretta: Il motivo per cui non è possibile usare questa struttura è dato dalla presenza di chiavi ripetute che generano un errore dell'interprete Javascript: in questo caso abbiamo infatti due elementi p
posti all'interno dello stesso oggetto radice.
Potremmo però modificare il codice rendendo uniche le chiavi, ad esempio utilizzando una convenzione che ci permetta di definire degli identificatori, eventualmente anche per separare del testo (innerHTML
) particolarmente lungo:
{ "p~1": { "cite": { "innerHTML": "'Le persone viaggiano ...", "title": "[cit. Sant'Agostino]" } }, "p~2": { "cite": { "innerHTML~1": "'Altra citazione...", "innerHTML~2": "... molto lunga", "title": "[cit. Autore]" } } }
In questo caso abbiamo utilizzato un carattere non alfabetico (tilde) per separare elemento da identificatore, rendendo uniche le chiavi che si trovano allo stesso livello. Quando la nostra funzione troverà una chiave del tipo <s1> ~ <s2>, considererà la sottostringa <s1> come l'elemento da creare.
Nella demo seguente è stata creata la seguente struttura:
var objNodes = { "p~1": { "cite": { "text~1": "'Le persone viaggiano per stupirsi delle montagne, dei fiumi, delle stelle", "text~2": "... e passano accanto a sé stesse senza meravigliarsi...'", "title": "[cit. Sant'Agostino]" } }, "p~2" : { "span": { "text":"Laghi dei Piani davanti al rifugio 'A.Locatelli', vicino alle ", "a": { "url":"http://it.wikipedia.org/wiki/Tre_Cime_di_Lavaredo", "text":"Tre Cime di Lavaredo", "title":"Vai alla pagina di wikipedia" } } }, "img": { "src": "laghi-dei-piani.jpg", "alt": "laghi dei piani" }, "form": { action: "#", method: "POST", "fieldset": { "label": { "text": "Scegli un rifugio attorno alle Tre Cime: ", "for": "listarifugi" }, "select": { "id": "listarifugi", "option~1": { "value": "", "text": "Lista dei Rifugi" }, "option~2": { "value": "http://it.wikipedia.org/wiki/Rifugio_Antonio_Locatelli", "text": "A.Locatelli (DreizinnenHutte)" }, "option~3": { "value": "http://it.wikipedia.org/wiki/Rifugio_Auronzo", "text": "Auronzo" }, "onchange": function() { if (this.value !== '') location.href = this.value; } } } } }
Nella demo 2 invece un esempio di inserimento iterativo di righe in una tabella.
Quindi ecco il codice che ci ha consentito di ottenere il corrispondente markup.
Come funziona?
La semplice libreria è stata scritta utilizzando il collaudato 'module pattern' (che ci consente di sfruttare i vantaggi derivanti da un approccio OOP) e implementando il 'metod-chaining' per utilizzare una sintassi più espressiva.
Il metodo create()
contiene una funzione che cicla sulla struttura oggetto e si occupa di creare ricorsivamente nodi ed attributi. Questi non vengono appesi direttamente su elementi in pagina o su nodi temporanei ma all'interno di un oggetto di tipo 'documentFragment'.
Quando la struttura è completamente creata all'interno del nostro frammento, la funzione ritorna l'istanza corrente e la struttura è pronta per essere inserita con il metodo pubblico append()
.
Il metodo append()
accetta come argomenti sia il tipo di inserimento, sia l'elemento di riferimento (una stringa id oppure un valore nodo, ad es. document.body
). Se ad esempio volessimo inserire la struttura precedente all'interno di un determinato <div>
, ad esempio
<div id="divnode"></div>
il codice Javascript necessario sarà
var DOMbuilder = new $DOM();// nuova istanza dell'oggetto if (DOMbuilder.create(objNodes).append('into', 'divnode')) { /* La struttura è stata correttamente inserita, ora posso scrivere codice che interagisce con i nodi creati. */ } else { /* errore: la struttura objNodes non è corretta o l'elemento di riferimento non esiste. */ }
Altri tipi di inserimento sono "before", "after" e "inside" (alias di "into"). Se non si specifica nessun elemento, per default verrà utilizzato quello specificato nella variabile privata _defaultNode
.
Ottimizzazione/1 : documentFragment
John Resig nel suo blog (a cui si rimanda per approfondimenti), ha recentemente osservato come l'utilizzo dell'oggetto documentFragment
sia più performante rispetto all'operazione di inserimento diretto sui nodi già presenti in pagina.
Questo oggetto, presente in tutti i browser più diffusi (IE6 compreso), è una sorta di nodo 'trasparente', un semplice oggetto neutro che viene allocato in memoria, al quale è possibile appendere altri nodi e che può essere appeso a sua volta, ma con il vantaggio di non creare inutili elementi intermedi.
Ottimizzazione/2 : cache dei nodi
Al crescere dei nodi da appendere aumenta la probabilità di dover creare lo stesso tipo di nodo (ad esempio 'div' o 'p'). Un'ottimizzazione possibile è quella di mantenere una sorta di cache privata in cui memorizzare i nostri prototipi di nodo e, quindi, crearne di volta in volta una copia con il metodo cloneNode()
:
/* 'el' è l'elemento nodo da appendere */ if (!_cache[el]) { _cache[el] = document.createElement(el); }; var elementNode = _cache[el].cloneNode(false);
Questa cache poi potrà essere sfruttata per una successiva generazione di nodi, utilizzando la stessa istanza dell'oggetto $DOM
. Sulla struttura usata nella demo la cache è entrata in funzione solo 3 volte (per un elemento
<p> e due elementi <li>) ma ciononostante ha migliorato i tempi mediamente del 6/8% su tutti i browser rispetto alla versione priva di cache.
Ottimizzazione/3 : innerHTML
Supponiamo di avere una struttura così definita:
{ "p" : { "cite" : { "title" : "[cit. Sant'Agostino]", "innerHTML" : "'Le persone viaggiano ..." }, "innerHTML" : "testo del paragrafo" } }
Se, dopo aver appeso l'elemento <cite>
applichiamo il metodo innerHTML
all'elemento
<p>
, sovrascriveremo l'elemento interno; per tale motivo, quando la funzione create()
incontra questa specifica chiave (o suoi shortcuts, vedi punto seguente) viene creato un nodo con il metodo createTextNode()
, che preserva gli eventuali contenuti già annidati.
Ottimizzazione/4 : shortcuts ed entità-unicode
Grazie alla variabile privata _shortProperties
abbiamo la possibilità di definire alias degli attributi, ad esempio 'class' per 'className' oppure 'text' per 'innerHTML'. In questo modo non sarà necessario ricordarsi l'esatta sintassi computed-style, soprattutto per attributi non usati di frequente ('for' per 'htmlFor').
Un'altra variabile privata _entityUnicode
ci dà invece la possibilità di inserire delle entities (à é ...) nei nodi di testo. Il metodo createTextNode()
infatti, in presenza di una entità HTML, scrive il codice per esteso invece di generare il carattere corrispondente. Tuttavia le sequenze unicode (uXXXX) sono invece interpretate correttamente.
Il metodo privato _createEntityReference()
, in modo del tutto trasparente sotituisce quindi le entità presenti nella variabile _entityUnicode
nelle corrispondenti sequenze unicode.
Performance
Per creare la struttura vista in precedenza (13 nodi e 21 attributi, uno contenente una funzione Javascript), i tempi medi hanno oscillato tra 0.429ms per Safari 3.1.1/Mac e oltre 5ms su Firefox 2.0.0.16/Win.
Di seguito una tabella, in ordine crescente, dei tempi medi (in millisecondi) su un ciclo di 1000 iterazioni/inserimenti* in diversi UserAgents/Platforms
- SF3.1.1/ Mac : 0.429
- SF3.1.2/ Win : 0.654
- OP9.51/ Win : 0.782
- OP9.20/ Mac : 2.045
- FF3.0.1/ Mac : 2.457
- FF3.0.1/ Win : 2.609
- IE7/Win : 3.368
- IE6/Win : 3.872
- FF2.0.0.16/Win : 5.196
[Test eseguiti su:
Mac: Intel Core Duo, 2.4GHz, Ram 2Gb, Mac OS X 10.5.3
Win: Intel, 1.60 Ghz, Ram 2Gb, Windows XP SP3]
in cui si evidenziano in positivo i tempi di esecuzione in Safari e in Opera su Win.
È interessante notare la differenza di velocità tra Firefox 2.0.0.16 e Firefox 3.0.1 dove quest'ultimo risulta essere quasi due volte più veloce del primo. Tuttavia Firefox 2, in media in 2 iterazioni su 3 ha registrato un tempo pari a 0ms, mentre nella terza iterazione il tempo era di 15/16ms (ecco lo screenshot del test con Firebug). Più regolare invece Firefox 3 come si può osservare dai tempi delle prime 25 iterazioni (anche in questo caso vi rimando alla schermata).
Modificare il DOM attraverso AJAX
Se si richiede al server un oggetto pure-object, una volta che avremo ottenuto la response, sarà necessario effettuare il parsing dell'oggetto stringa prima di essere utilizzata dal metodo create()
.
Tecnicamente è possibile farlo utilizzando il metodo eval()
, ma questo approccio è fortemente sconsigliato (e qui, per tale motivo, volutamente non trattato), soprattutto per questioni legate alla sicurezza.
In attesa di Javascript 2.0 basato su ECMAscript 4 - che metterà a disposizione un metodo nativo per il parsing di stringhe JSON - è tutt'ora possibile utilizzare 'json2', la funzione creata da Douglas Crockford.