Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Il problema di window.onload

Panoramica di approfondimento sulle varie tecniche per aggiungere interazione alle pagine appena il contenuto è disponibile
Panoramica di approfondimento sulle varie tecniche per aggiungere interazione alle pagine appena il contenuto è disponibile
Link copiato negli appunti

Questa è la traduzione dell'articolo The window.onload problem (still) di Peter Michaux, pubblicato originariamente su Peter's blog il 30 gennaio 2007. La traduzione viene qui presentata con il consenso dell'autore.

L'obiettivo della programmazione secondo i dettami del Javascript non intrusivo è quello di separare l'interazione (Javascript) dal contenuto (HTML), ed è analogo all'obiettivo dell'uso dei CSS: tenere separato il livello della presentazione (CSS) dal livello del contenuto (HTML).

La separazione della presentazione dal contenuto è in voga e praticata da anni, ma c'è invece qualche difficoltà nel tentativo di separare completamente il livello dell'interazione. Questo articolo affronta le tecniche che sono state proposte nel corso del tempo al fine di ottenere questa separazione. Di ciascuna tecnica analizzeremo punti di forza e punti di debolezza.

Il problema

Supponiamo di avere questo semplice documento HTML:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Search Engines</title>
</head>
<body>
<h1>Search Engines</h1>
<ul style="list-style-type:square;">
<li onclick="alert('Google');">Google</li>
<li onclick="alert('Yahoo!');">Yahoo!</li>
</ul>
<p><img src="hawaii.jpg" alt="hawaii"></p>
</body>
</html>

Possiamo chiaramente vedere che nel documento HTML, CSS e Javascript sono mescolati insieme. Significa e ne consegue che quelli che creano i contenuti, chi crea la presentazione e gli stili e quanti si occupano dell'interazione devono lavorare sullo stesso documento. Questa pagina manca di modularità e chi lavorerà su di essa si troverà spesso a scontrarsi.

Ciò che vorremmo ottenere, dunque, è la separazione di contenuto, presentazione e interazione. Separare contenuto e presentazione è possibile con un CSS esterno. Separare l'interazione con un Javascript esterno, invece, è più complicato. Nel contesto di questo articolo, il nostro obiettivo sarà quello di rimuovere cose come onclick, onmouseover, etc, dal codice HTML. Se ci riusciamo, avremo realizzato il nostro fine: un Javascript non intrusivo (vedremo poi che provare ad implementare più complesse manipolazioni della pagina sarà causa di problemi più grandi).

La user experience della pagina vista qui sopra è quella che vogliamo e ciò è importante da ricordare. In base a come i browser gestiscono il tutto, dal momento in cui la pagina è visibile l'utente può cliccare su uno degli item della lista e vedrà apparire un alert.

La difficoltà di window.onload

Il precedente esempio e quanto appena detto, insomma, mostrano che di fatto non avremmo bisogno di effettuare la separazione tra i tre livelli (tutto funziona lo stesso), ma che se vogliamo possiamo. Potremmo allora creare tre file separati, ciascuno per il contenuto, la presentazione e l'interazione. Ecco l'HTML:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Search Engines</title>
<link href="presentation.css" rel="stylesheet" type="text/css">
<script src="behavior.js" type="text/javascript"></script>
</head>
<body>
<h1>Search Engines</h1>
<ul>
<li id="google">Google</li>
<li id="yahoo">Yahoo!</li>
</ul>
<p><img src="hawaii.jpg" alt="hawaii"></p>
</body>
</html>

Ed ecco invece il file presentation.css:

ul {
list-style-type:square;
}

Per finire il file behavior.js:

window.onload = function() {
document.getElementById('google').onclick = function()   {alert('Google');};
document.getElementById('yahoo').onclick = function()   {alert('Yahoo!');};
}

Certamente è un buon esempio di separazione. Se l'autore del codice HTML fornisce sufficienti agganci nel markup sotto forma di attributi id e class, allora chi lavora sul CSS e il programmatore Javascript sapranno a cosa agganciarsi nel contesto della pagina. Il CSS è applicato automaticamente dal browser. L'evento window.onload, invece, consente al programmatore di dare vita alla pagina aggiungendo gli eventi giusti agli elementi giusti. L'aggiunta dell'interazione avviene quando finiscono il caricamento della pagina, il parsing e il rendering della stessa e quando viene attivato l'evento window.onload. Tutti i problemi nascono intorno a quest'ultimo 'quando'.

L'immagine hawaii.jpg che abbiamo inserito nella pagina è molto grande e pesante, e l'evento window.onload non viene attivato fin quando l'immagine stessa non è completamente caricata. Purtroppo, il testo con i nomi dei due motori di ricerca sarà visibile all'utente per un certo periodo di tempo prima che window.onload sia stato attivato. Su molte pagine reali questo periodo potrà essere purtroppo lungo da 1 a 10 secondi, talvolta anche di più. È un tempo sufficiente per far sì che l'utente possa dare un'occhiata alla pagina, cliccare sul nome di uno dei due motori di ricerca e non vedere apparire l'alert.

Insomma, per raggiungere la modularità cui si accennava e ottenere i benefici del Javascript non intrusivo, abbiamo rovinato oltre il dovuto l'esperienza dell'utente. Abbiamo così bisogno di una tecnica per cui si possa dar vita agli elementi prima che l'utente abbia la possibilità di interagire con la pagina. Se non riusciamo a trovare una soluzione soddisfacente, allora dovremmo ritornare alla versione iniziale, con gli attributi per gli eventi incorporati nell'HTML, a tutto beneficio dell'esperienza del nostro visitatore.

Mettere lo script in fondo alla pagina

Questa è una tecnica vecchia, certamente robusta, ma è pur sempre un compromesso che non soddisfa pienamente i puristi del Javascript non intrusivo. Ignorando per un momento la separazione della presentazione, possiamo almeno separare tutto il Javascript dall'HTML usando l'elemento script in fondo al corpo della pagina per inizializzare l'interazione sulla pagina.

L'idea alla base di tutto è che dal momento in cui il browser fa il parsing di questo elemento script finale ed esegue il Javscript che esso contiene, gli elementi che lo precedono saranno stati già parsati e disponibili alla manipolazione con il DOM. Il funzionamento della tecnica si basa sul comportamento de facto standard di tutti i browser e potrebbe non soddisfare in pieno alcuni sviluppatori che seguono invece la lettera delle specifiche. Comunque, molti sviluppatori si affidano a questo approccio, che funziona in tantissimi script. Questo il file HTML:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Search Engines</title>
<script src="behavior.js" type="text/javascript"></script>
</head>
<body>
<h1>Search Engines</h1>
<ul>
<li id="google">Google</li>
<li id="yahoo">Yahoo!</li>
</ul>
<p><img src="hawaii.jpg" alt="hawaii"></p>
<script type="text/javascript">init();</script>
</body>
</html>

Ed ecco il file behavior.js:

function init() {
document.getElementById('google').onclick = function() {alert('Google');};
document.getElementById('yahoo').onclick = function() {alert('Yahoo!'););
}

È persino difficile notare quei sette caratteri di Javascript in fondo alla pagina HTML. È una separazione tutto sommato buona e tutti sappiamo che sviluppando per il web si devono spesso accettare piccoli compromessi. Il problema è che anche dopo aver sviluppato la millesima pagina con questo metodo, ci ritroveremo tante volte a constatare di aver dimenticato di includere lo script in fondo alla pagina che richiama la funzione init().

Se vogliamo aggirare questo problema potenziale (dimenticarsi dello script in fondo alla pagina), possiamo seguire il metodo che segue. Ad ogni modo, infatti, se ci dimentichiamo, l'interazione sarà comunque aggiunta tramite l'evento window.onload():

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Search Engines</title>
<script src="behavior.js" type="text/javascript"></script>
</head>
<body>
<h1>Search Engines</h1>
<ul>
<li id="google">Google</li>
<li id="yahoo">Yahoo!</li>
</ul>
<p><img src="hawaii.jpg" alt="hawaii"></p>
<script type="text/javascript">window.onload();</script>
</body>
</html>

var alreadyRun = false;
window.onload = function() {
if (alreadyRun) {return;}
alreadyRun = true;
document.getElementById('google').onclick = function() {alert('Google');}
document.getElementById('yahoo').onclick = function() {alert('Yahoo!');}
}

Nell'esempio visto qui sopra ci premuniamo davanti ad uno scenario: che l'interazione venga aggiunta due volte alla pagina. È un tema ricorrente che affronteremo dopo.

Si noti pure che non è necessario aggiungere lo script esattamente sul fondo della pagina. Deve soltanto essere messo dopo l'ultimo elemento che sarà attivato tramite Javascript (nel nostro caso l'ultimo elemento li).

A questo punto potremmo essere tutti soddisfatti, dal momento che questa tecnica svolge tutto sommato bene il suo compito. Ma possiamo eliminare anche quel pezzettino di Javascript dal codice HTML per ottenere una perfetta separazione? Molte persone ci hanno provato e, come si diceva all'inizio, questo articolo intende esplorare proprio le tecniche che rendono tutto ciò possibile. Tra l'altro, quando discuteremo di document.readyState più avanti vedremo che anche questa vecchia tecnica dello script in fondo non è esente da difetti. Vale quindi la pena continuare a leggere.

Dean Edwards e il browser sniffing

Sul suo blog Dean Edwards ha postato il seguente script per consentire un'immediata aggiunta dell'interazione ad una pagina (prima cioè che ciò avvenga con l'evento window.onload). Lo script e la dettagliata analisi che lo accompagna sono importanti perché note librerie come jQuery, MooTools e Low Pro for Prototype includono proprio questo codice (nota del traduttore: alcuni commenti nel codice sono stati tradotti in italiano):

function init() {
// interrompi se questa funzione è stata già chiamata
if (arguments.callee.done) return;
// contrassegna questa funzione in modo da non fare due volte la stessa cosa
arguments.callee.done = true;
// interrompi il timer
if (_timer) clearInterval(_timer);
// esegui
};
/* per Mozilla/Opera9 */
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", init, false);
}
/* per Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=__ie_onload defer src=javascript:void(0)></script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
if (this.readyState == "complete") {
init(); // call the onload handler
}
};
/*@end @*/
/* per Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
init(); // call the onload handler
}
}, 10);
}
/* per gli altri browser */
window.onload = init;

La funzione init() deve essere eseguita non appena il DOM è completamente costruito e disponibile. L'ultima riga usa window.onload: è la più solida alternativa a cui appoggiarsi e in cui poter sperare e sarà in fin dei conti eseguita rivelando ogni errore di sintassi o di runtime.

Lo script funziona nelle attuali (gennaio 2007) versioni di Mozilla (ovvero Firefox), Internet Explorer, Webkit (ovvero Safari) e Opera. Sono i quattro grandi browser e se si riesce ad essere compatibili con questi, almeno per ora, allora vuol dire che abbiamo fatto un buon lavoro.

Come si evince dal codice, lo script poggia su meccanismi diversi in base al browser. Nelle prossime sezioni dell'articolo analizzeremo questi meccanismi adottati per dare subito interattività alla pagina e i problemi generati da essi.

Mozilla, Opera e DOMContentLoaded

A partire da Netscape Navigator 7 e da Firefox 1.0, i browser basati su Mozilla offrono l'evento DOMContentLoaded. Anche Opera 9 lo ha aggiunto. È esattamente l'evento che cercavamo. Ci dice quando il DOM è pronto per essere manipolato e disponibile. Essendo un evento, possiamo attaccarci più gestori di eventi con document.addEventListener() e ottenere una buona modularità nel codice. Torneremo su questo evento quando parleremo di una nuova soluzione che chiude questo articolo.

Internet Explorer e l'attributo defer

Internet Explorer è uno dei pochi browser che riconoscono l'attributo defer di un tag script. Le specifiche sono molto precise riguardo il significato e l'utilizzo di questo attributo:

Quando è definito, questo attributo booleano suggerisce al browser che lo script non sta per generare alcun contenuto nel documento (per esempio che non c'è alcun document.write nel Javascript) e così il browser può continuare con il parsing e il rendering del documento.

La parola 'suggerisce' è volutamente vaga. Dice allo sviluppatore di non dipendere da una particolare reazione del browser al defer. La specifica non dice che lo script deve essere differito fino a quando il DOM non è pronto e disponibile. Il suggerimento consentirà, probabilmente, al browser di ottimizzare la visualizzazione della pagina nel modo in cui esso vorrà.

Ci sono poi molti argomenti che mostrano come questo attributo non sia un buon indicatore dell'avvenuto caricamento del DOM e del fatto che esso sia pronto per fornire interattività alla pagina.

Ancora più importante è che un particolare comportamento in risposta all'attributo defer in una versione di un browser non dovrebbe essere assunto come identico in un altro browser o in un'altra versione dello stesso browser. L'implementazione di defer non è standardizzata e dovremmo evitare di affidarci a questo attributo per verificare quando il caricamento del DOM è completo.

Internet Explorer e document.readyState

Internet Explorer ha introdotto la proprietà non-standard document.readyState con i seguenti stati:

Stato Significato
uninitialized L'oggetto non è stato inizializzato con dati.
loading L'oggetto sta caricando i suoi dati.
loaded L'oggetto ha finito di caricare i suoi dati.
interactive L'utente può interagire con l'oggetto anche se non è stato completamente caricato. (read-only)
complete L'oggetto è completamente inizializzato.

Internet Explorer 6 restituisce uno stato di loading mentre il documento effettua il parsing, costruisce l'albero DOM e carica le immagini. Ho fatto un piccolo esperimento facendo un polling di document.readyState sia per lo stato loaded sia per lo stato complete. Confrontando i risultati emersi da simili esperimenti effettuati sulla tecnica dello script in fondo alla pagina e sulla tecnica del defer, è dimostrato che document.readyState non restituisce uno stato loadedcomplete fin quando non sono state caricate anche le immagini. Tutto avviene, insomma, piuttosto tardi, più o meno nello stesso tempo impiegato dall'evento window.onload.

Internet Explorer 7 restituisce uno stato di interactive mentre il documento effettua il parsing, costruisce l'albero DOM e carica le immagini. Ne consegue che lo stato complete è il primo che ci garantisce che il DOM è pronto. Purtroppo questo stato viene restituito solo dopo che le immagini sono state completamente caricate, ovvero molto tardi e alcuni esperimenti dimostrano che lo stato complete viene restituito più o meno nello stesso tempo necessario con l'evento window.onload.

Insomma, per più di una ragione usare document.readyState in Internet Explorer non aiuta a raggiungere l'obiettivo che ci siamo prefissati. Ma possiamo comunque imparare qualcosa di molto importante da document.readyState.

Internet Explorer 7 sembra trovarsi nello stato interactive fino all'evento window.onload. Lo stato interactive è di sola lettura. Significa che, se proviamo a fare qualcosa con il DOM o con i suoi oggetti prima di window.onload, rischiamo di andare incontro al fallimento. È triste, perché significa che non è possibile contare su una tecnica robusta che dia immediatamente interattività alla pagina su tutti e quattro i browser principali.

La questione si complica molto a questo punto. Jack Slocum e The Doctor What hanno entrambi affermato che provare ad operare ampie manipolazioni del DOM durante questo stato di interactive e prima di window.onload può causare seri problemi. Hanno anche riferito, però, che in qualche modo lo script di Edwards e l'uso del defer riescono a individuare un momento nel processo di caricamento della pagina su Internet Explorer 7 in cui queste ampie manipolazioni sul DOM non producono errori. Non è comunque qualcosa su cui io mi baserei. L'obiettivo di questo articolo è di ottenere un Javascript non intrusivo che, come minimo, riesca a farci eliminare gli attributi onclick, onmouseover, etc, dal codice HTML. Nessuno ha riferito di problemi derivati dall'aggiunta di queste proprietà in un qualunque momento durante lo stato interactive. Possiamo allora andare avanti.

Webkit e DOMContentLoaded

Webkit non implementa l'evento non-standard DOMContentLoaded. Simon Willison ha creato un ticket per DOMContentLoaded e c'è una patch in attesa di essere applicata da molto tempo sul trac dello stesso Webkit. Il ticket non ha ricevuto nessun voto. Se volete che Webkit implementi questo evento, date il vostro voto o visitate il canale IRC #webkit per farlo sapere agli sviluppatori.

Uno degli sviluppatori di Webkit mi ha fatto sapere che loro supporteranno più probabilmente l'attributo defer dell'elemento script, e ciò potrebbe aiutare a risolvere il problema di window.onload.

Sinceramente non trovo che sia una buonissima notizia per via della diversità dei due approcci. L'attibuto defer è concepito per dare una sorta di suggerimento al browser su una possibile opportunità di ottimizzare il caricamento della pagina. Usare un apparente comportamento del browser che differisce l'azione fino a quando il DOM non è pronto è al massimo un hack. L'obiettivo di DOMContentLoaded, invece, è quello di dare agli sviluppatori un segnale preciso che quello è il momento giusto per agire sul DOM e se un browser implementa questo evento è senz'altro più affidabile.

Webkit e document.readyState

Webkit è un progetto open source alla base del motore di rendering usato su Safari e altri browser. Webkit implementa anche il document.readyState di Internet Explorer. Qui sotto trovate il codice webkit per document.readyState (revisione 19086; 26 gennaio 2007):

701 String Document::readyState() const
702 {
703 if (Frame* f = frame()) {
704 if (f->loader()->isComplete())
705 return "complete";
706 if (parsing())
707 return "loading";
708 return "loaded";
709 // FIXME: What does "interactive" mean?
710 // FIXME: Missing support for "uninitialized".
711 }
712 return String();
713 }

Il codice è per fortuna abbastanza leggibile e osservandolo emergono alcuni importanti dettagli.

Lo stato loaded viene restituito quando il DOM è completamente parsato e disponibile. Più tardi, quando tutte le immagini sono state caricate nel frame, viene restituito lo stato complete. Dunque, al momento, e solo per Webkit, possiamo fare un polling su document.readyState, e se ci vengono restituiti lo stato loaded o anche lo stato complete sappiamo che il DOM è pronto.

Vediamo anche che non tutti i possibili readyStates sono implementati. Per quanto abbiamo già detto in precedenza, osserviamo che lo stato interactive non viene mai restituito. Internet Explorer restituisce a volte interactive, per cui Webkit e Internet Explorer sono ora incompatibili. Capiremo più avanti perché questa incompatibilità è importante.

Il significato dei vari stati implementati è un po' confuso. Mentre il documento sta effettuando il parsing e sta costruendo il DOM restiturà loading mentre noi ci aspetteremmo interactive. Gli sviluppatori di Webkit mi hanno spiegato che Webkit fa il parsing dell'HTML mentre esso arriva a pezzi al browser. Ciò significa che Webkit non procederà in sequenza attraverso gli stati di loading e poi di parsing. Per documenti che richiedono molti pezzi, c'è un periodo di tempo in cui Webkit si trova simultaneamente negli stati di loading e interactive. In questo frangente, Webkit restituisce come stato loading, cosa che è legittima; ma Internet Explorer 7 restituisce interactive.

Ancora più importante è che il codice su document.readyState di Webkit quasi certamente sarà modificato nel futuro. Gli sviluppatori mi hanno riferito che, essendo document.readyState un'estensione di Internet Explorer, l'implementazione in Webkit dovrà adeguarsi ad essa. In pratica, un giorno Webkit potrà essere modificato per restituire lo stato interactive mentre il documento sta effettuando il parsing. Significa che solo lo stato complete potrà indicarci che il DOM è pronto. Avvenendo tutto ciò, come si è visto, dopo il caricamento delle immagini, è troppo tardi. La sostanza è che dipendere dal valore di document.readyState restituito da Webkit non è molto consigliabile, soprattutto se si ragiona in termini di longevità del codice Javascript.

Quale funzionalità dei browser utilizzare?

Se vogliamo programmare secondo i dettami del Javascript non intrusivo allora sappiamo che possiamo contare su window.onload() come nostra alternativa estrema. Sappiamo che possiamo dar vita alla pagina più rapidamente sulle versioni recenti di Mozilla e Opera grazie a DOMContentLoaded. La tecnica del defer di Internet Explorer potrebbe rivelarsi piuttosto fragile se il browser deciderà di aderire in toto alle specifiche. La tecnica del document.readyState cambierà molto probabilemente nelle prossime versioni di Webkit per diventare compatibile con Internet Explorer 7. Purtroppo, quindi, i metodi per adeguare Internet Explorer e Webkit a Mozilla e Opera sono piuttosto fragili, e ci sono ancora altri browser da prendere in considerazione.

Abbiamo anche imparato dallo stato di sola lettura della proprietà document.readyState che un'aggiunta troppo precoce dell'interazione alla pagina è rischiosa, a prescindere dalla tecnica adottata.

Lo script che Dean Edwards ha postato consente sì una completa separazione del Javascript dall'HTML, ma purtroppo, alla lunga, i potenziali risvolti negativi sono troppi se confrontiamo la purezza di questa soluzione con la solidità della tecnica dello script sul fondo della pagina.

Polling del DOM

La libreria dedicata agli eventi della Yahoo User Interface Library offre due funzioni che possono aiutarci nell'implementazione di un Javascript non intrusivo. Queste funzioni non presentano i problemi visti nella tecnica dello script in fondo alla pagina e nello script di Dean Edwards. Invece di provare a determinare quando l'intero DOM è pronto, la libreria fa un polling sul DOM con document.getElementById() fino a quando un particolare elemento non viene trovato o fino a quando non scatta l'evento window.onload. Quando document.getElementById() restituisce quel particolare elemento allora significa che esso è presente nel DOM e a quel punto dovrebbe essere sicuro aggiungere eventi all'elemento. Questa del polling è una grande idea. Il concetto può funzionare in browser vecchi come Internet Explorer 4, Netscape Navigator 4 e Opera 5, sebbene nella YUI Library esso non sia stato implementato per funzionare con questi browser.

Questa tecnica sembra essere robusta solo quando si tratta di aggiungere eventi ma per altri complesse manipolazioni del DOM può essere problematica.

onAvailable()

Il seguente esempio è simile a YAHOO.util.Event.onAvailable() ma senza le opzioni extra offerte dalla libreria di Yahoo:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Search Engines</title>
<script src="onAvailable.js" type="text/javascript"></script>
<script src="behavior.js" type="text/javascript"></script>
</head>
<body>
<h1>Search Engines</h1>
<div><ul id="engines">
<li id="google">Google</li>
<li id="yahoo">Yahoo!</li>
</ul></div>
</body>
</html>

Codice di onAvailable.js:

var stack = [],
interval,
loaded; // has window.onload fired?
function doPoll() {
var notFound = [];
for (var i=0; i<stack.length; i++) {
if (document.getElementById(stack[i].id)) {
stack[i].callback();
} else {
notFound.push(stack[i]);
}
}
stack = notFound;
if (notFound.length < 1 || loaded) {
stopPolling();
}
}
function startPolling() {
if (interval) {return;}
interval = setInterval(doPoll, 10);
}
function stopPolling() {
if (!interval) {return;}
clearInterval(interval);
interval = null;
}
function onAvailable(id, callback) {
stack.push({id:id, callback:callback});
startPolling();
}
window.onload = function() {
loaded = true;
doPoll();
};

Codice di behavior.js:

onAvailable('google', function(){
document.getElementById('google').onclick = function() {alert(this.id);};
});
onAvailable('yahoo', function(){
document.getElementById('yahoo').onclick = function() {alert(this.id);};
});

Nell'esempio visto qui sopra il Javascript è stato suddiviso in due parti per mostrare il codice da libreria di onAvailable.js e l'uso del codice di questa libreria su una pagina particolare in behavior.js.

Il codice di behavior.js è ripetitivo e se la lista dei motori di ricerca nella pagina HTML cresce fino a comprenderne più di due, tutto ciò sarebbe intollerabile. Un altro problema potrebbe essere che la pagina viene creata dinamicamente con un numero variabile di motori di ricerca nella lista. Forse gli elementi li hanno una classe e solo quegli elementi dovrebbero essere attivati. La libreria deve poter gestire queste situazioni, e la YUI Library lo fa.

onContentAvailable()

La funzione YAHOO.util.Event.onContentReady() della libreria YUI risolve per certe situazioni i problemi in cui si può incorrere con onAvailable(). La funzione onContentAvailable() è simile a onAvailable() ma dichiara un elemento come disponibile quando il suo elemento nextSibling è a sua volta presente e caricato nel DOM. Se non viene trovato nessun nextSibling, allora un elemento è presentato come disponibile solo dopo che viene attivato l'evento window.onload.

Perché aspettare per un nextSibling? Ritorniamo all'esempio da cui siamo partiti. Supponiamo di fare un polling sul DOM per la lista non ordinata con id="engines". Quando questo elemento viene trovato nel DOM non è necessariamente vero che anche tutti i suoi elementi figli siano presenti e disponibili nel DOM. Il parser HTML potrebbe aver effettuato il parsing solo del primo elemento della lista e non del resto. Se la lista con id="engines" ha un elemento nextSibling esistente nel DOM, allora si può essere sicuri sul fatto che il parser ha finito di creare l'intera lista e che tutti i suoi item sono disponibili nel DOM.

Nell'esempio visto, purtroppo, non c'è un elemento dopo la lista con id="engines" che possa fare da nextSibling. I tag di chiusuta </ul> e </div> non hanno nemmeno uno spazio tra di essi. In questo caso, onContentAvailable() deve aspettare che scatti l'evento window.onload, dal momento che non c'è modo di sapere se il DOM è completo. Per aiutare, diciamo così, onContentAvailable() a dare vita e interazione alla pagina, dobbiamo aggiungere un elemento dopo la lista.

Potremmo risolvere il problema semplicemente aggiungendo uno spazio tra i tag di chiusura. Questo spazio svolgerà la funzione di un nodo di testo e potrà essere usato come nextSibling. Alcuni browser, però, potrebbero non aggiungre tale nodo di testo al DOM in presenza di questo spazio bianco non necessario tra i tag. Inoltre, se stiamo cercando di ottimizzare il codice HTML rimuovendo tutti gli spazi bianchi, potremmo avere bisogno di aggiungere un elemento extra dopo la lista, in questo modo:

<div><ul id="engines">
<li id="google">Google</li>
<li id="yahoo">Yahoo!</li>
</ul><div></div></div>

Riempire la pagina con elementi vuoti come questo, però, non è certo la nostra intenzione. Possiamo evitare tutto ciò aggiungendolo, questo elemento vuoto, in fondo alla pagina. Quando viene effettuato il polling, se l'elemento che ci interessa non ha un nextSibling, possiamo attraversare i suoi elementi antenati per verificare che uno di essi abbia un nextSibling. Inserendo un elemento vuoto in fondo alla pagina, siamo sicuri dal momento in cui il DOM ha completato il parsing che almeno uno degli antenati ha un nextSibling e possiamo concludere che il contenuto di tutti gli elementi è disponibile.

Attraversando l'albero è altamente probabile che almeno uno degli antenati abbia un nextSibling proprio, per cui è anche improbabile il caso per cui ci sia bisogno di un elemento vuoto come quello descritto. AL libreria per gli eventi della YUI non attraversa gli antenati. L'esempio che segue aggiunge anche l'uso dell'evento DOMContentLoaded().

Ecco un esempio in cui tutti i concetti fin qui espressi sono stati messi insieme. Cominciamo con l'HTML:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">

<head>
<title>Search Engines</title>
<script src="onContentAvailable.js" type="text/javascript"></script>
<script src="behavior.js" type="text/javascript"></script>
</head>

<body>
<h1>Search Engines</h1>
<ul id="engines">
<li id="google">Google</li>
<li id="yahoo">Yahoo!</li>
</ul>
<div></div>
</body>

</html>

Ed ecco il codice di onContentAvailable.js:

var stack = [],
interval,
loaded; // has DOMContentLoaded or window.onload fired
// does the element or one of it's ancestors have a nextSibling?
function hasNextSibling(el) {
return el.nextSibling ||
(el.parentNode && hasNextSibling(el.parentNode));
}
function doPoll() {
var notFound = [];
for (var i=0; i<stack.length; i++) {
var el = document.getElementById(stack[i].id);

if (el && (hasNextSibling(el) || loaded)) {
stack[i].callback();
} else {
notFound.push(stack[i]);
}
}
stack = notFound;
if (notFound.length < 1 || loaded) {
stopPolling();
}
}
function startPolling() {
if (interval) {return;}
interval = setInterval(doPoll, 10);
}
function stopPolling() {
if (!interval) {return;}
clearInterval(interval);
interval = null;
}
function onContentAvailable(id, callback) {
stack.push({id:id, callback:callback});
startPolling();
}
function lastPoll() {
if (loaded) {return;}
loaded = true;
doPoll();
}
// Force one poll immediately when the document DOMContentLoaded event fires.
// This may be sooner than the next schedualed poll.
// Can't add this listener in at least Firefox 2 through DOM0 property assignment.
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', lastPoll, false);
} else if (document.attachEvent) {
// optimistic that one day Internet Explorer will support this event
document.attachEvent('onDOMContentLoaded', lastPoll);
}
// Force one poll immediately when window.onload fires. For some pages if
// the brower doesn't support DOMContentLoaded the window.onload event
// may be sooner than the next schedualed poll.
window.onload = lastPoll;

Per finire, behavior.js:

onContentAvailable('engines', function(){
var list = document.getElementById('engines').childNodes;
for (var i=0; i<list.length; i++) {
list[i].onclick = function() {alert(this.id);};
}
});

Se si fa il confronto con onAvailable(), usare onContentAvailable() è molto meglio per l'esempio delle liste che abbiamo visto. Entrambe le funzioni sono utili, ma onAvailable() è più efficiente solo per trovare un unico elemento. Il codice di behavior.js inoltre può manipolare un numero variabile di elementi. Con il polling, poi, bisogna verificare un solo elemento invece che ciascuno degli item della lista. Il tempo risparmiato potrà essere usato, se necessario, per attraversare il DOM alla ricerca di un nextSibling.

Conclusioni

L'inserimento degli attributi per la gestione degli eventi nel codice HTML è la tecnica più robusta, ma non ci garantisce una perfetta separazione dell'interazione dalla struttura.

Lo stato read-only interactive della proprietà document.readyState rende quasi impossibile l'aggiunta precoce dell'interattività alla pagina, ma non sono stati riportati problemi rispetto all'aggiunta di event listener.

La tecnica dello script in fondo alla pagina funziona con tutti i browser ma rapprasenta un compromesso rispetto alla totale separazione di interazione e struttura HTML. Bisogna poi sempre ricordarsi di aggiungere l'elemento script in fondo alla pagina.

Lo script di Dean Edwards consente una separazione completa ma è problematico rispetto al futuro. Browser vecchi e poco diffusi non aggiungeranno interazione alla pagina prima dell'evento window.onload.

Il polling del DOM è cross-browser e consente pure lui la completa separazione. In casi estremamente rari si dovrà usare un elemento vuoto per aiutare il codice a a sapere quando il contenuto d manipolare è disponibile. In Internet Explorer 7, il polling del DOM può trovare elementi prima che essi siano spostati nel DOM, tuttavia non sembrano esserci problemi nell'aggiunta di event listener.

Ciò di cui abbiamo veramente bisogno è che i produttori di browser capiscano l'importanza del Javascript non intrusivo e che pertanto ci diano un metodo standard per dare vita agli elementi della pagina appena essi appaiono. Sarebbe una cosa analoga ai selettori CSS usati per formattare gli elementi appoggiandosi a id e classi.


Ti consigliamo anche