#43 AJAX with RJS
In questo episodio useremo RJS per aggiungere un po’ di funzionalità AJAX al sito. RJS è il modo più semplice per usare AJAX con Rails, specialmente se occorre aggiornare più di un elemento nella pagina.
Nella pagina dei prodotti del nostro sito, gli utenti possono aggiungere opinioni su un prodotto mediante una form. Quando viene fatto il submit della form, viene eseguita una richiesta HTTP POST al server remoto e la pagina viene ricaricata.
La nostra pagina dei prodotti che mostra le opinioni.
Dopo che la form è stata inviata cambiano diversi elementi nella pagina originaria: il testo che mostra il numero di opinioni è cambiato, l’opinione è stata aggiunta alla lista, la form è stata resettata e c’è un messaggio in cima alla pagina che ringrazia l’utente per avere aggiunto l’opinione. Tutto ciò dovrà essere aggiornato dal nostro template RJS dopo che avremo AJAXificato la form.
Modifiche alla vista
Prima di cominciare ad aggiornare il codice della vista, dobbiamo sincerarci che stiamo includendo prototype e gli altri file standard JavaScript. Per farlo, aggiungiamo questa linea alla sezione <head>
del file di layout:
<%= javascript_include_tag :defaults %>
Cominciamo ora a modificare il codice della vista. Ecco come appare ora, secondo una forma piuttosto standard per una vista Rails:
<% form_for [@product, @review] do |form| %> <ol class="formList"> <li><%= form.label :name, "Name:" %> <%= form.text_field :name %></li> <li><%= form.label :content, "Review:" %> <%= form.text_area :content, :rows => 5 %></li> <li><%= submit_tag "Add comment" %></li> </ol> <% end %>
Per fare in modo che la form invii una richiesta AJAX è sufficiente sostituire form_for
nella prima riga con form_remote_for
. Rails fornisce un numero di metodi che rendono i ‘normali’ elementi di una form in elementi AJAX-enabled; per maggiori informazioni è disponibile una lista di tali metodi sul sito delle API di Rails1.
Dopo avere eseguito la modifica ed aggiornata la pagina, il tag di apertura della form avrà un attributo onsubmit
che consentirà il submit asincrono della form stessa. Se il sito fosse utilizzato da qualcuno che non avesse il JavaScript abilitato sul browser, la form continuerebbe a funzionare, ma alla stessa maniera in cui funzionava prima (cioè con una normale POST HTTP).
<form action="/products/1/reviews" class="new_review" id="new_review" method="post" onsubmit="new Ajax.Request('/products/1/reviews', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;”>
Modifiche al controller
La form dell’opinione viene inviata ad un action chiamata create
nel controller delle opinioni. Questa action ha il compito di creare una nuova opinione, impostare una notifica flash e ridirigere indietro alla pagina dei prodotti:
def create @review = Review.new(params[:review]) @review.product_id = params[:product_id] @review.save flash[:notice] = "Thanks for your review!" redirect_to product_path(params[:product_id]) end
Il redirect non funzionerebbe con una richiesta AJAX, per cui modifichiamo il metodo per fare in modo che il controller risponda in maniera differente a seconda di quale effettiva richiesta gli arriva (HTTP oppure JavaScript). Il metodo respond_to
ci permette di intervenire su questa cosa. Prende un blocco in cui mettiamo il codice per ogni formato possibile. Sostituiamo il redirect_to
nella action precedente con questo:
respond_to do |format| format.html { redirect_to product_path(params[:product_id]) } format.js end
Ora, con questo codice, la redirect agirà ancora correttamente per le richieste che giungono alla action mediante HTTP, ma non ci sarà per quelle inoltrate mediante AJAX. Poichè non c’è codice associato al blocco js, si ricadrà direttamente ad un template RJS.
Creazione di un template RJS
Il file di template va messo nella cartella /app/views/reviews
e, poichè è eseguito dalla action create
, dovrà chiamarsi create.rjs
. Il file RJS produrrà il JavaScript e lo restituirà al client in risposta alla richiesta AJAX.
La prima cosa che dobbiamo fare è aggiungere una nuova opinione alla lista. Le opinioni sono renderizzate sottoforma di lista ordinata per id
di review
. Ogni opinione è creata in un partial chiamato _review.html.erb
. Il codice RJS per aggiungere la nuova opinione in fondo alla lista è il seguente:
page.insert_html :bottom, :reviews, :partial => 'review', :object => @review
Il codice mostrato usa insert_html
per aggiungere HTML alla pagina. Gli argomenti che prende indicano:
- Dove inserire l’HTML (si può inserire
:before
,:after
,:top
o:bottom
). - L’
id
dell’elemento che vogliamo modificare (in questo caso la nostra lista di opinioni, ‘reviews’). - L’HTML da renderizzare. Questa parte può essere semplicemente del testo, oppure, come nel nostro caso, un partial. Passiamo il nome del partial e l’oggetto da passare a quest’ultimo. Stiamo renderizzando una nuova opinione, per cui passiamo il partial
review
e il nuovo oggetto opinione creato2.
La nuova opinione sarà aggiunta alla lista al submit della form, ma il testo che mostra il numero di opinioni non sarà aggiornato. Aggiungiamo dell’altro codice RJS per farlo. Questa volta sostituiremo dell’HTML già esistente piuttosto che aggiungerne di nuovo, per cui useremo la replace_html
:
page.replace_html :reviews_count, pluralize(@review.product.reviews.size, 'Review')
Alla replace_html
occorre fornire l’id
dell’elemento da aggiornare e il nuovo contenuto. Passiamo un testo usando lo stesso metodo pluralize
usato nel codice della vista. Non avendo visibilità della variabile @product
che abbiamo usato nel caso della vista, dobbiamo prenderci il prodotto associato all’opinione e il numero di opinioni che ha.
Vogliamo anche resettare la form una volta che una nuova opinione è stata aggiunta. La form ha per id
"new_review
" e possiamo usare l’oggetto page
per passare un metodo JavaScript ad essa nel seguente modo:
page[:new_review].reset
Infine, dobbiamo mostrare un messaggio flash. Come abbiamo fatto per l’elemento che mostra in numero di opinioni, possiamo di nuovo usare la replace_html
per mostrare il messaggio:
page.replace_html :notice, flash[:notice]
Ultimo problema
La nostra form ora funziona come volevamo e le opinioni possono essere aggiunte senza la necessità di fare una POST completa e ricaricare l’intera pagina. C’è ancora un piccolo problema da risolvere, comunque. Se si esegue un refresh della pagina a seguito di un inserimento, il messaggio flash rimarrà visibile, benchè scompaia ad un ulteriore refresh. Questo accade a causa del meccanismo in cui funziona la messaggistica flash: il messaggio si mantiene per una richiesta.
Il modo per aggirare questo problemino consiste semplicemente nello scartare il flash subito dopo che lo si è passato al JavaScript per il rendering. E’ dunque sufficiente aggiungere un’altra linea al nostro file RJS:
flash.discard
Ora il messaggio flash non apparirà nuovamente a seguito di un refresh della pagina o di una navigazione in un’altra, conseguente all’aggiunta di un’opinione.
La nostra pagina si comporta ora esattamente come prima, ma senza la post back. Il file RJS finale appare così:
page.insert_html :bottom, :reviews, :partial => 'review', :object => @review page.replace_html :reviews_count, pluralize(@review.product.reviews.size, 'Review') page[:new_review].reset page.replace_html :notice, flash[:notice] flash.discard
- http://api.rubyonrails.org/classes/ActionView/Helpers/PrototypeHelper.html
- In Rails 2.3 tutto ciò può essere abbreviato con
:partial => @review
.