#197 Nested Model Form Part 2
- Download:
- source codeProject Files in Zip (121 KB)
- mp4Full Size H.264 Video (17.1 MB)
- m4vSmaller H.264 Video (12.4 MB)
- webmFull Size VP8 Video (34.2 MB)
- ogvFull Size Theora Video (23.6 MB)
Nel precedente episodio abbiamo mostrato come creare un form che possa gestire più modelli annidati. Nell’applicazione creata a tal fine abbiamo un modello Survey
(sondaggio), ogni Survey
ha molte Questions
(domande) e ogni Question
molte Answers
(risposte).
Nei modelli Survey
e Question
abbiamo utilizzato accepts_nested_attributes_for
per consentirci di creare, modificare ed eliminare i record annidati attraverso un unico modello.
Nello stato attuale l’applicazione è strutturata in modo tale che se vogliamo eliminare una domanda o una risposta, dobbiamo utilizzare una checkbox. Inoltre non c’è nessun modo per aggiungere nuove domande o risposte attraverso il form. In questo episodio risolveremo questi problemi utilizzando JavaScript per modificare il form in modo tale da usare link per la creazione ed eliminazione dinamica dei modelli.
Il codice JavaScript che scriveremo implica una manipolazione del DOM, useremo quindi la Prototype library per rendere il tutto più facile. Per aggiungere il codice Prototype alla nostra applicazione basta aggiungere la seguente riga alla sezione <head> del nostro file di layout dell’applicazione.
<%= javascript_include_tag :defaults, :cache => true %>
Se preferite utilizzare jQuery potete farlo, vi mostreremo il codice jQuery equivalente alla fine dell’episodio.
Aggiungere i Link per Rimuovere le Risposte
Affronteremo per prima la parte più facile: rimpiazzare le checkbox che attualmente utilizziamo per rimuovere le risposte con dei link. Diamo prima un’occhiata alle risposte.
Il codice che visualizza ogni risposta è contenuto in un partial chiamato answer_fields
che ha il seguente contenuto:
<p> <%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %> </p>
Nel codice appena elencato quando vogliamo eliminare una risposta selezioniamo la checkbox _destroy
. La rimpiazzeremo ora con un campo nascosto il cui valore verrà impostato al clic sul link “remove”. In questo modo possiamo continuare a selezionare una risposta perchè venga eliminata.
Sostituiremo alla label del codice un link e faremo uso della funzione Rails’ link_to_function
per creare un link che al click esegua una funzione JavaScript. Con il campo nascosto e il link nel codice del partial il codice diventa:
<p class="fields"> <%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.hidden_field :_destroy %> <%= link_to_function "remove", "remove_fields(this)" %> </p>
Facendo click sul link “remove” posizionato accanto a una risposta, verrà eseguita una funzione JavaScript chiamata remove_fields
, nella quale passiamo il link stesso come argomento così da poterlo utilizzare come elemento di riferimento per trovare gli altri campi in relazione con la risposta. Non c’è un modo diretto per raggiungere tutti i campi così abbiamo aggiunto un attributo class al tag <p> che li contiene, per trovarli più facilmente.
Ora ci manca di scrivere la funzione remove_fields
. Lo faremo nel file application.js
che viene automaticamente incluso nelle nostre pagine in conseguenza della chiamata JavaScript :defaults
nel nostro application layout.
function remove_fields(link) { $(link).previous("input[type=hidden]").value = "1"; $(link).up(".fields").hide(); }
La funzione effettua due azioni. Primo, utilizza la funzione previous
di Prototype per trovare il primo hidden field che precede il link che chiama la funzione, ossia il campo _destroy, e imposta il suo valore a 1
così che la risposta venga segnata come cancellata. Utilizza quindi la funzione up
per risalire l'albero del DOM dal link fino a quando non trova un elemento con l’attributo class uguale a fields
, che è il nome che abbiamo dato al paragrafo che incapsula tutti i campi della risposta, e lo nasconde in modo tale che la risposta non risulti più visibile.
Se ora ricarichiamo la pagina del sondaggio vedremo un link accanto ad ogni risposta.
Se facciamo click su qualsiasi link il valore dell’hidden field _destroy
sarà impostato a 1
per quelle risposte, che risulteranno quindi selezionate per l’eliminazione, e i loro campi nel form verranno nascosti.
Da notare il fatto che non stiamo utilizzando AJAX per effettuare il post back dei valori aggiornati nel form al click del link, quindi nonostante le risposte vengano immediatamente nascoste, il database non verrà aggiornato fino al submit del form. L’invio del form renderà effettiva l’eliminazione delle risposte e potremo vedere il risultato nella pagina show
del sondaggio.
Eliminare le Domande
Ora che possiamo eliminare le risposte con dei link ci possiamo occupare delle domande. La tecnica utilizzata sarà la stessa usata per eliminare una risposta e quindi possiamo riutilizzare una parte del codice che già abbiamo scritto.
Come abbiamo già fatto per le risposte rimpiazzeremo checkbox e label con un hidden field e un link, possiamo quindi copiare questa parte del codice dal partial answer_fields
e metterlo un in metodo helper che chiameremo link_to_remove_fields
, passandogli il testo che vogliamo appaia nel link e la variabile f
del form.
<p class="fields"> <%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= link_to_remove_fields "remove", f %> </p>
Scriveremo ora il metodo nel file application_helper
.
# Methods added to this helper will be available to all templates in the application. module ApplicationHelper def link_to_remove_fields(name, f) f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)") end end
I metodi che creano campi nel form restituiscono stringhe, possiamo quindi concatenare l’HTML generato dai metodi f.hidden_field
e link_to_function
e restituirlo al partial.
Possiamo utilizzare il nostro nuovo metodo anche nel partial question_fields
.
<div class="fields"> <p> <%= f.label :content, "Question" %> <%= link_to_remove_fields "remove", f%><br /> <%= f.text_area :content, :rows => 3 %><br /> </p> <% f.fields_for :answers do |builder| %> <%= render 'answer_fields', :f => builder %> <% end %> </div>
Poichè la nostra funzione JavaScript remove_fields
cerca un elemento con class fields
per nascondere una domanda o una risposta, abbiamo incapsulato tutto il partial in un elemento div
con quella class così che quando viene fatto click sul link “remove” per una domanda questa venga nascosta insieme alle sue risposte.
Se guardiamo la pagina edit
di un sondaggio e facciamo click sul link “remove” di una domanda, questa verrà rimossa con tutte le sue risposte e all’invio del form la domanda e le sue risposte verranno eliminate dal sondaggio.
Aggiungere Domande e Risposte
Passiamo alla parte difficile: aggiungere nuove domande e risposte. Vogliamo che nel form appaiano dei link per l'aggiunta di domande e risposte. Questi ultimi una volta cliccati devono creare nuovi campi di testo dinamicamente. La difficoltà risiede nel fatto che il codice JavaScript, al click dei link, deve accedere ad un insieme di campi vuoti che permettano di inserire le nuove domande e risposte.
Per fare questo scriveremo un nuovo metodo nell'application helper chiamato link_to_add_fields
.
In questo modo potremo usare questo metodo ogni volta che sarà necessario inserire nel form un link per aggiungere una nuova domanda o risposta. Il codice per il metodo potrebbe essere questo:
def link_to_add_fields(name, f, association) new_object = f.object.class.reflect_on_association(association).klass.new fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder| render(association.to_s.singularize + "_fields", :f => builder) end link_to_function(name, h("add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")")) end
Il metodo riceve tre argomenti in ingresso: name
, che corrisponderà al testo del link; f
, l'oggetto form builder e association
, che in questo caso potrà assumere il valore di “questions” o “answers” a seconda del contesto.
La prima riga di codice del metodo crea una nuova istanza della classe association, ovvero una nuova Domanda (Question)
o Risposta (Answer)
. Questo significa che da questo momento in poi, nel codice, avremo a disposizione un oggetto template che potremo usare per creare nuovi campi del form.
La seconda parte del codice costruisce una stringa composta dai campi del form corrispondenti all'oggetto associato (Domanda o Risposta). Questa stringa sarà inserita nella funzione JavaScript, che aggiungerà tali campi al form nel momento in cui viene cliccato un link, chiamando il rispettivo partial e passandogli l'oggetto form builder. L'unica novità in questo caso è il fatto che abbiamo impostato un :child_index
. Lo abbiamo fatto per avere qualcosa a cui riferirci all'atto della creazione di una nuova domanda o risposta.
Nel codice JavaScript lo sostituiremo con un valore univoco basato sull’orario attuale (current time). In questo modo ogni nuova domanda o risposta creata, sarà dotata di un indice univoco e potrà essere identificata quando il form viene inviato.
Infine abbiamo usato ancora il metodo link_to_function
, passandogli il nome del link e una chiamata alla funzione JavaScript add_fields
alla quale viene passato il link stesso, il nome dell'associazione e una stringa contenente i campi del form da aggiungere che hanno subito l'escape (N.d.r "escape" = L'abilità di processare una stringa per far si che i caratteri < e & trasformati in < e &, rispettivamente, siano interpretati come tali e non come markup HTML).
Ora possiamo dedicarci alla scrittura del codice JavaScript, ovvero della funzione add_fields
.
function add_fields(link, association, content) { var new_id = new Date().getTime(); var regexp = new RegExp("new_" + association, "g"); $(link).up().insert({ before: content.replace(regexp, new_id) }); }
Questa funzione riceve in ingresso i tre argomenti di cui abbiamo parlato prima: il link che è appena stato cliccato, il nome dell'associazione e la stringa contenente l'HTML per i campi del form. La funzione, prima di tutto, crea un id per ogni nuovo elemento del form. Questo è necessario per evitare che le doomande o risposte, create nel medesimo form, abbiano lo stesso indice e siano considerate identiche (stesso oggetto model), quando vengono inserite e salvate. Usando l'orario corrente, facciamo si che possiedano id univoco e mediante un'espressione regolare lo sostituiamo alla stringa “new_question” o “new_answer” nei campi del form. Il risultato così ottenuto viene inserito al posto giusto, nel DOM.
La parte difficile finisce qui. Ora dobbiamo solo aggiungere i link stessi al form. Nel partial che abbiamo chiamato question_fields
metteremo un link per aggiungere una nuova risposta, usando link_to_add_fields
, e passandogli :answers
come nome dell'associazione dato che una domanda può avere molte risposte.
<div class="fields"> <p> <%= f.label :content, "Question" %> <%= link_to_remove_fields "remove", f %><br /> <%= f.text_area :content, :rows => 3 %><br /> </p> <% f.fields_for :answers do |builder| %> <%= render 'answer_fields', :f => builder %> <% end %> <p><%= link_to_add_fields "Add Answer", f, :answers %></p> </div>
Analogamente faremo con il form del sondaggio per mettere il link “Add Question”.
<% form_for @survey do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% f.fields_for :questions do |builder| %> <%= render 'question_fields', :f => builder %> <% end %> <p><%= link_to_add_fields "Add Question", f, :questions %> <p><%= f.submit "Submit" %></p> <% end %>
Se aggiorniamo la pagina form del sondaggio troveremo anche i collegamenti per aggiungere nuove domande o risposte e cliccandone uno apparirà un nuovo campo nel form.
Un nuovo campo vuoto appare nel form quando clicchiamo su “Add Answer”.
Se riempiamo il nuovo campo con la parola “jQuery” e inviamo il form, verrà aggiunta una nuova risposta.
Abbiamo così raggiunto il nostro obiettivo: un form sul quale possiamo aggiungere o togliere nuovi campi. Le nuove aggiunte o rimozioni saranno anch'esse salvate sul database, quando inviamo il form.
Codice alternativo basato su jQuery
Il codice JavaScript che abbiamo usato in questo episodio si basa sulla libreria Prototype. Se preferite usare jQuery, il codice potrebbe essere questo:
function remove_fields(link) { $(link).prev("input[type=hidden]").val("1"); $(link).closest(".fields").hide(); } function add_fields(link, association, content) { var new_id = new Date().getTime(); var regexp = new RegExp("new_" + association, "g"); $(link).parent().before(content.replace(regexp, new_id)); }
E' molto simile al codice scritto in precedenza usando Prototype.
Alcuni di voi potrebbero essere delusi dal fatto che il codice JavaScript che abbiamo usato non è unobtrusive. Anche se è sempre preferibile una soluzione unobtrusive, non sarebbe comunque così semplice da inserire in un episodio introduttivo come questo. Ryan Bates sta lavorando ad un plugin che si chiama nested_form che fa uso di jQuery per gestire form annidati in modo unobtrusive. Lo sviluppo di questo plugin è ancora in fase embrionale per cui se ritenete di aver bisogno necessariamente di un sistema unobtrusive tenete sotto controllo il progetto per vedere a che punto è arrivato e se fa per voi.