Come si costuisce una tabella in javascript per contenere dei dati? Quelle canoniche tabelle con una paccata di dati estratti da una query? E come si dà un modo comodo all'utente di navigarla?
Intendo per esempio la possibilità di ordinare i risultati in base a qualche attributo, piuttosto che paginare il risultato per non dover vedere 5000 righe di tabella in un colpo solo?
La soluzione canonica è usare PHP (o qualunque altro linguaggio lato server) per strutturare la tabella, aggiungere un po' di campi nascosti di una forma che vengono popolati da un po' di click in giro per la pagina e usati per inviare una richiesta al server per ottenere un altro ordinamento dei dati, piuttosto che l'accesso ad un altro subset di dati.
È una soluzione, effettivamente. È una buona soluzione, anzi, che presenta alcuni notevoli vantaggi rispetto a quella che sto per descrivere, ma anche qualche svantaggio. Ma non è l'unica: si possono fare quasi tutte queste operazioni anche lato client, con un po' di sano Javascript, l'aiuto di DHTML e un approccio meno “old-style” al problema.
Prima di esaminare una possibile soluzione alternativa facciamo un attimo il gioco dei pro e dei contro delle due soluzioni, tanto per allenare la mente e confermarci, se mai ce ne fosse bisogno, che non esiste una soluzione sempre migliore, dipende quasi sempre tutto dal contesto.
Soluzione lato Server.
Pro:
- è tradizionale, un sacco di gente lo fa, si può eveitare di spremere il cervello.
- In caso di paginazione si possono inviare via rete pochi dati, relativi alla sola pagina visualizzata
- In caso di ordinamento è facile che il server abbia algoritmi più performanti di quelli di javascript
Contro:
- senza la paginazione, ma con ordinamento, a ogni ordinamento si passano tutti i dati, potenzialmente parecchi
- anche se la paginazione lato client è meno performante come algoritmo, è nulla rispetto alla latenza di una chiamata e risposta al server, quindi il risultato è quasi sempre molto più brillante
- il server diventa un collo di bottiglia. Con o senza paginazione rischia o di dover estrarre in continuazione i dati e inviarli o di doverli tenere in cache per ogni utente, con uno sforzo mostruoso (e una conseguente spesa non indifferente per dimensionare l'hardware...)
Soluzione lato Client:
Pro:
- è un po' più trendy, più web 2.0, come dicono le persone alla moda (soprattutto quelle che non sanno di cosa parlano)
- permette di cambiare impostazioni e interagire in modo molto più rapido
- usa le risorse del client, spesso più libero nel contesto di naviazione web, e occupa il server solo nel caricamento iniziale
Contro:
- i dati vengono trasferiti tutti subito, quindi il primo caricamento potrebbe essere più lento
- si usano le risorse del client, quindi la navigazione con un catorcio risulta poco agile
- le differenze prestazionali fra browser sono notevolissime, quindi una soluzione che appare ottima per prestazioni ed aspetto con opera o firefox potrebbe (beh, dai, il condizionale lo uso anche se il periodo ipotetico è purtroppo della realtà) risultare pessima con IE.
Insomma si sposta il peso di alcune operazioni. Cosa si meglio dipende dallo stile di navigazione degli utenti, ma anche, e soprattutto, dal tipo e dalla mole di dati presentati.
Detto questo presento un piccolo “widget”, se così vogliamo chiamarlo, per ora prototipale, che mi sono realizzato per studio e per comodità di riutilizzo.
La mia soluzione è sviluppata in 3 classi ed è basata su jQuery, che permette una veloce e buona manipolazione del DHTML senza dover inseguire le differenze fra browser e ha delle API molto comode.
La prima classe che introduco è il contenitore dei dati, che con grande fantasia ho chiamato Container. Vediamolo:
Se non sapete nulla sulle classi in javascript, può essere utile prima approfondire questo argomento (ma per i nostri scopi basta dire, in modo improprio, che una funzione può essere il costruttore di un oggetto e che a quella funzione si possono agganciare dei metodi tramite la definizione di prototipi), altrimenti passiamo subito all'analisi del contenitore.
Quello che un contenitore deve fare è mostrare una serie di righe contenenti i risultati, permettere di ordinare per alcune (potenzialmente tutte, ma dipende dal contesto, e a volte nessuna) colonne, e spezzare il risultato in più pagine quando ci sono troppi dati.
Per costruire un nuovo Container necessitiamo di questi parametri:
htmlcontainerid: id dell'elemento html in cui vogliamo posizionare il contenitore. Tipicamente sarà un div, ma può essere quello che serve che sia.
myid: id del contenitore stesso. Non è in realtà poi così importante ai nostri fini, serve per ritrovarlo con i canonici metodi uasti da altri script nella pagina.
obja: array degli elementi contenuti. I contenuti sono oggetti di una classe, che vedremo più avanti.
colnames: array con i nomi delle colonne da mostrare.
Facciamo ora una veloce carrellata dei metodi utili in questo contenitore:
SetRowNumColumn: Questo metodo serve per impostare il nome della colonna che indica il numero di riga. Se viene impostato ad undefined o a qualunque cosa che in javascript dia False, elimina la colonna con il numero di riga.
SetPager: imposta il paginatore. Non l'abbiamo ancora visto, ma il paginatore è una classe usata dal contenitore per alcune operazioni. Se si passa undefined si elimina il paginatore.
SetSortList: richiede l'elenco delle colonne per cui si può ordinare, come elenco di numeri, senza contare quella opzionale dei numeri di riga. Il parametro depth al momento non è usato (ma deve comunque essere maggiore di 0), mentre i 2 successivi parametri sono le immagini (o quel che si vuole) da usare come frecce sulla colonna per cui si ordina.
DrawAll: non dovrebbe mai essere necessario chimarlo se non dopo l'impostazione del comportamento desiderato, ma è comunque sempre accessibile per forzare un ridisegno del contenitore.
SetColNames: imposta il nome delle colonne. Dopo che sono state impostate il contenitore non si ridisegna automaticamente, quindi il cambio al volo di questi nomi potrebbe essere un caso di necessità di utilizzo della DrawAll.
SetClassName: imposta il nome della classe da usare per il contenitore, ai meri fini di creazione di un foglio di stile consono.
Niente allarmi, è tutto piuttosto semplice, in realtà!
Passiamo dunque a vedere l'oggetto contenuto, eccolo:
Anche se fornisce una implementazione minima, l'ho comunque chiamato IContained, perchè vorrebbe essere, più che altro, una interfaccia.
C'è anche un costruttore super ridotto che permette l'utilizzo vero e proprio di questa classe. Questi i suoi argomenti:
uid: questo è un id (univoco?) dell'elemento, previsto perchè è assai probabile che i vari oggetti esposti abbiano un identificativo con cui riconoscersi, utile speciamente nelle eventuali successive chiamate al server.
va: array di valori, per le singole colonne.
Ecco i suoi pochi metodi utili, li descrivo perchè sono quelli di cui più facilmente si farà override in classi derivate:
DrawAsRow: viene chiamato dal Container per dire all'oggetto di restituire la sua rappresentazione sulla riga. In questa versione l'oggetto suppone di dover scrivere i contenuti dell'argomento va del costruttore in ordine come li ha ricevuti, uno per colonna, senza manipolazioni.
GetValueByPos: chiamato dal Container in fase di ordinamento, richiede all'elemento di restituire il valore di una certa colonna (argomento). L'implementazione di base restituisce quello che l'oggetto stamperebbe in quella colonna, nulla vieta di modificare il comportamento per restituire un altro valore magari più performante per l'ordinamento o semplicemente diverso.
Come si vede è una classe minimale, con pochi fronzoli, ma abbastanza sostanza da stare in piedi.
Passiamo dunque all'ultima classe, il paginatore, GPager. Eccolo:
Il costruttore vuole questi paramentri:
uid: questa volta è verametne univoco...o così dovrebbe essere. Usato per poterlo ritrovare nella pagina per altri script.
container: riferimento al conenitore, viene usato quando cambia qualcosa nel pager e questo ha effetti sul contenuto da mostrare, per esempio quando si cambia pagina.
itemsperpage: numero di righe per pagina. Più propriamente numero di elementi, visto che potenzialmente potrebbero anche disegnarsi su più righe.
showpagesatedge: quante pagine mostrare vicino ai bordi? Il canonico far sempre vedere la prim e l'ultima, o le prime 3 e le ultime 3...canonici parametri di navigazione.
showpagesatposition: quante pagine mostrare nel pager adiacenti a quella attuale? Quella prima e quella dopo o le 3 prima e le 3 dopo? Altro canonico parametro di navigazione.
Non ci sono metodi utili da chiamare dall'esterno, mentre magari come estenderlo lo vediamo in una puntata successiva. Ci sono invece du parametri direttamente accessibili che posono essere cambiati e sono prevsymbol e nextsymbol che sono i simboli usati per fare previous e next page, altra caratteristica standard di navigazione
Come accennato, anche questa classe può essere specializzata per apparire più bella o per avere un comportamento più evoluto.
Per provare il pager con dei contenuti bisogna salvare le 3 classi in uno o più file (in ho usato un unico file chiato “Icontainer.js”) e scaricare e mettere in un posto accessibile anche una versione non preistorica di jQuery (io ho usato la 1.2.6). Modificate come necessario i link a questi file.
Ed ora ecco la paginetta per provare il comporamento del pager con dei contenuti:
ah, quasi dimenticavo....i link rotti ad immagini che vedete è perchè uso 'uparr.gif' e 'dwarr.gif' come freccia in su e freccia in giù. Scegliete su internet delle frecce che vi piacciano e cambiate i riferimenti come preferite (possono essere anche degli url remoti).
Come ultima note, se vedete che IE ci mette circa 1 minuto a ordinare 10000 elementi, laddove opera ci mette pochi secondi e FF è istanteneo, non scappate...pensate solo che è il caso di cambiare browser...e per stare sul sicuro si può sempre lasciare l'ordinamento al server ;)
Come rendere il tutto più presentabile lo vediamo in una prossima puntata con un po' di css!