#228 Sortable Table Columns
- Download:
- source codeProject Files in Zip (107 KB)
- mp4Full Size H.264 Video (17 MB)
- m4vSmaller H.264 Video (11.5 MB)
- webmFull Size VP8 Video (30.4 MB)
- ogvFull Size Theora Video (25.2 MB)
In questo episodio vi mostreremo come rendere una tabella ordinabile cliccando una delle sue colonne. Di sotto è riportata una pagina, presa dall’applicazione del negozio online, che mostra un elenco di prodotti all’interno di una tabella. Vorremmo poter ordinare gli elementi nella tabella cliccando su una delle intestazioni delle colonne. In questo caso non sarebbe molto utile, vista la quantità esigua di prodotti, ma più in generale, in presenza di molti record all’interno di una tabella paginata, la possibilità di ordinare per colonna può rivelarsi molto utile.
Ci sono molti plugin disponibili che potremmo utilizzare in questo contesto, come ad esempio Searchlogic, trattato nell’episodio 176 [guardalo, leggilo], ma non useremo nessuno di questi in questa sede: faremo tutto da noi.
Creazione dei link
Partiremo dal codice della vista per l’action index, che contiene la tabella che mostra i prodotti:
<% title "Products" %> <table class="pretty"> <tr> <th>Name</th> <th>Price</th> <th>Released</th> </tr> <% for product in @products %> <tr> <td><%= product.name %></td> <td class="price"><%= number_to_currency(product.price, :unit => "£") %></td> <td><%= product.released_at.strftime("%B %e, %Y") %></td> </tr> <% end %> </table> <p><%= link_to "New Product", new_product_path %></p>
La maggior parte delle modifiche che dovremo fare rigurarderanno le celle di testata della tabella. Vogliamo che il testo riportato su di esse divenga un link, in modo tale da permettere l’ordinamento opportuno della tabella al click su di esse. Procederemo per piccoli passi, facendo le cose più, semplici che possano funzionare a ciascun passo. In questo modo vedremo evolvere il processo man mano che faremo dei progressi.
La prima cosa che faremo sarà rendere le interstazioni delle colonne della tabella dei links, per cui dovremo aggiungere un link_to
prima del testo di ogni cella di testata. (Possiamo fare il tutto in modo molto rapido in TextMate, tenendo premuto il tasto option e scegliendo le tre colonne in fondo al tag di apertura <th>
. Qualsiasi tipo di testo che digiteremo verrà di conseguenza aggiunto a tutte e tre le linee). La pagina verso cui vogliamo navigare mediante questi nuovi link è la stessa da cui partiamo, questa, ma con dei parametri di query diversi. Dobbiamo quindi definire un hash e passarlo come secondo parametro alla link_to
:
<th><%= link_to "Name", :sort => "name" %></th> <th><%= link_to "Price", :sort => "price" %></th> <th><%= link_to "Released", :sort => "released_at" %></th>
Al ricaricamento della pagina dei prodotti, le testate della tabella si saranno trasformante in link e, se ci mettiamo il cursore sopra, vedremo l’opportuno paramero nella query dell’URL del link:
Ordinamento dei prodotti
Per far funzionare l’ordinamento, dobbiamo cambiare la action index
del ProductController
in modo tale che ordini i prodotti in base al parametro di ordinamento presente nella stringa della query:
def index @products = Product.order(params[:sort]) end
Abbiamo usato il metodo order
di Rails 3 in questo caso per ordinare i prodotti, ma se fossimo in un’applicazione Rails 2, avremmo potuto usare il find
insieme ad un hash, per definire il criterio di ordinamento. Si noti che stiamo passando direttamente dei parametri utente alla clausola order; questa cosa in genere non andrebbe mai fatta, perchè l’input dell’utente non è stato controllaro e per questo motivo siamo esposti ad attacchi di tipo SQL injection. (Questo argomento è stato trattato nell’episodio 25 [guardalo, leggilo].) Per ora trascuriamo questa cosa: ci ritorneremo su in un secondo momento per sistemarla.
Ora l’ordinamento funzionerà: se, una volta ricaricata la pagina, clicchiamo su una delle intestazioni di colonna della tabella dei prodotti, i record verranno ordinati a seconda del campo su cui abbiamo cliccato:
Invertire l’ordine
Abbiamo fatto un bel po’ di progressi fino ad ora, senza dover scrivere molto codice, ma ci sono ancora un po’ di funzionalità da aggiungere, come ad esempio la possibilità di invertire l’ordine di una colonna ricliccando nuovamente sulla stessa e mostrando una icona a forma di freccia indicante il tipo di ordinamento corrente.
Affrontiamo per prima cosa l’inversione dell’ordinamento. Ricliccando sullo stesso link della testata di colonna su cui avevamo cliccato in precedenza, si vorrebbe poter vedere la tabella riordinata in base allo stesso campo, ma con ordine inverso. Mettere tutta questa logica direttamente nella vista aggiungerebbe troppa logica inline e creerebbe inutili duplicazioni, per cui decidiamo di impementare il tutto in un metodo helper responsabile della creazione di ciascun link di ordinamento.
Chiamiamo il nuovo metodo sortable
. Accetterà due argomenti, il secondo dei quali sarà opzionale. Il primo argomento sarà il nome della colonna ed il secondo sarà il testo di testata della colonna, nel caso in cui quest’ultimo debba essere diverso dal nome della colonna. Le testate della nostra tabella ora appariranno così:
<tr> <th><%= sortable "name" %></th> <th><%= sortable "price" %></th> <th><%= sortable "released_at", "Released" %></th> </tr>
Implementiamo il metodo sortable
nel module ApplicationHelper
:
module ApplicationHelper def sortable(column, title = nil) title ||= column.titleize direction = (column == params[:sort] && params[:direction] == "asc") ? "desc" : "asc" link_to title, :sort => column, :direction => direction end end
Il metodo ha i due argomenti sopra citati, con l’argomento title
che ha un default di nil
, in modo tale che sia possibile impostare un valore in base all’argomento column se solamente questo viene passato al metodo. Poi abbiamo la logica che determina che tipo di ordinamento debba avere il link. Se la colonna per la quale stiamo generando il link corrisponde a quella che ha determinato l’ordinamento corrente e la direzione di ordinamento corrente è crescente, allora dobbiamo impostare la direzione a desc
, in modo tale che la prossima volta che il campo viene cliccato, la colonna sia ordinata all’altra maniera. In tutti gli altri casi vogliamo che l’ordinamento sia crescente. Capita la direzione dell’ordinamento, possiamo aggiungerla come parametro al link.
Se ricarichiamo la pagina e clicchiamo sul link “Name”, la tabella verrà inizialmente ordinata per nome in ordine crescente. Quando però ora riclicchiamo nuovamente sullo stesso link “Name”, il parametro direction
cambia al valore desc
nella stringa della query, ma la tabella non viene ordinata in modo decrescente per nome:
La tabella non viene riordinata secondo la nuova direzione, poichè non stiamo prendendo in considerazione il parametro direction nella action del controller che evade la richiesta. Per sistemare la cosa, dobbiamo semplicemente aggiungere la direzione fra i parametri passati al metodo order
nel momento in cui recuperiamo tutti i prodotti, ossia nella action index
del ProductController
:
def index @products = Product.order(params[:sort] + ' ' + params[:direction]) end
Di nuovo, si noti come passare dei parametri provenienti dall’utente direttamente a una query SQL, come in questo caso, non sia una prassi sicura: ci ritorneremo su più tardi per correggere questa falla. Al ricaricamento della pagina, comunque sia, gli elementi sono ordinati in modo corretto ed ora si può cliccare su qualsiasi colonna per ordinare i record in base a criteri di ordinamento crescenti o decrescenti di tale colonna:
Aggiunta dei valori di default
Anche se ora la tabella sembra funzionare a dovere, se proviamo a rimuovere la stringa di query dell’URL provando ad andare direttamente alla pagina dei prodotti, otteniamo un errore, dal momento che il codice nel controller prova a leggere i valori dei parametri dalla stringa di query. Siccome in questo caso i parametri sono entrambi nil
, viene lanciato l’errore nel momento in cui il codice prova a unirli insieme come stringa.
Dobbiamo impostare alcuni valori di default per i parametri sort
e direction
. Potremmo cambiare l’hash dei parametri params
direttamente, impostando entrambi i valori se non esistono all’interno della query string, ma anzichè fare così, scriveremo due metodi nel controller che restituiranno il parametro oppure il valore di default, nel caso in cui il parametro non sia contenuto nella stringa di query. Poi useremo questi metodi per costruire l’argomento dell’ordinamento:
class ProductsController < ApplicationController helper_method :sort_column, :sort_direction def index @products = Product.order(sort_column + ' ' + sort_direction) end private def sort_column params[:sort] || "name" end def sort_direction params[:direction] || "asc" end end
Dobbiamo rendere disponibili questi due metodi all’ApplicationHelper
, affinchè possano essere utilizzati all’interno del metodo sortable
ivi definito poc’anzi, per cui li abbiamo dichiarati entrambi come metodi helper. Ora non ci resta che modificare il codice del metodo sortable
per far sì che utilizzi questi due nuovi metodi:
module ApplicationHelper def sortable(column, title = nil) title ||= column.titleize direction = (column == sort_column && sort_direction == "asc") ? "desc" : "asc" link_to title, :sort => column, :direction => direction end end
Ora se visitiamo la pagina dei prodotti senza indicare alcun parametro, la pagina utilizzerà il default, ossia l’ordinamento dei prodotti per nome crescente. Cliccando sul link “Name” la prima volta, si otterrà il riordinamento dell’elenco di prodotti per ordine decrescente di nome, come ci aspettavamo che avvenisse:
Rendere la query sicura
Come già detto in precedenza, non è una buona idea passare l’input proveniente dall’utente direttamente alla query per il database, come fatto nel caso della clausola di ordinamento, perchè ciò espone al rischio di SQL injection. Dobbiamo rendere sicure le stringhe di input provenienti dall’utente prima che queste siano passate alla clausola di ordinamento. Potremmo fare della pulizia generica sui parametri, ma invece seguiremo un approccio più stringente su ciò che è permesso passare.
Poichè abbiamo scritto dei metodi accessor per la colonna di ordinamento e per la direzione, possiamo aggiungere del codice in questi metodi, che garantisca che i valori restituiti per l’ordinamento siano validi e sicuri. La direzione dell’ordinamento può assumere solo due valori noti a priori, per cui per quel parametro possiamo controllare che il valore proveniente dall’utente faccia match con uno dei due e, se così non fosse, impostiamo il valore di default asc
.
def sort_direction %w[asc desc].include?(params[:direction]) ? params[:direction] : "asc" end
Possiamo fare qualcosa di simile nel metodo sort_column
, per assicurarci che il parametro di ordinamento faccia match con uno dei campi del modello Product, usando come valore di default “name” se il match fallisce:
def sort_column Product.column_names.include?(params[:sort]) ? params[:sort] : "name" end
Potreste voler essere ancor più restrittivi, vincolando l’ordinamento solamente ad alcune colonne nella tabella, ma l’approccio appena proposto può bastare per i nostri scopi.
Indicare il campo di ordinamento corrente
Concludiamo l’episodio aggiungendo un icona accanto al campo determinante l’ordinamento corrente, per indicare la direzione dell’ordinamento. Potremmo farlo coi CSS, ma per prima cosa dobbiamo sistemare il metodo helper sortable
in modo tale che aggiunga il nome di una classe al link nella testata del corrente campo di ordinamento:
module ApplicationHelper def sortable(column, title = nil) title ||= column.titleize css_class = (column == sort_column) ? "current #{sort_direction}" : nil direction = (column == sort_column && sort_direction == "asc") ? "desc" : "asc" link_to title, {:sort => column, :direction => direction}, {:class => css_class} end end
Nel metodo abbiamo ora aggiunto una variabile css_class
. Se la colonna corrente è anche quella che ordina, quella variabile ha un valore o pari a “current asc
” o a “current desc
” a seconda della direzione attuale di ordinamento, altrimenti viene impostata a nil
.
Nel codice che genera il link aggiungiamo poi l’attributo class
. Affinchè l’attributo non non sia aggiunto alla stringa di query, è necessario che separiamo il parametro in due hash. In questo modo l’attributo verrà aggiunto come attributo del tag ancora.
Abbiamo già aggiunto i due file di immagine che useremo nella cartella /public/images
, per cui dobbiamo solo aggiungere una classe CSS al foglio di stile della nostra applicazione, affinchè sia mostrata l’immagine corretta:
.pretty th .current { padding-right: 12px; background-repeat: no-repeat; background-position: right center; } .pretty th .asc { background-image: url(/images/up_arrow.gif); } .pretty th .desc { background-image: url(/images/down_arrow.gif); }
Quando riordiniamo ora la tabella, la colonna che ordina la tabella è ora contraddistinta da una freccia che indica il verso di ordinamento:
E’ tutto per questo episodio. Ora abbiamo ciò che volevamo: una tabella che possiamo ordinare in base ad una qualsiasi delle sue colonne con un indicatore che contraddistingue quale sia la colonna di ordinamento corrente.