#196 Nested Model Form Part 1
- Download:
- source codeProject Files in Zip (100 KB)
- mp4Full Size H.264 Video (16.1 MB)
- m4vSmaller H.264 Video (11.1 MB)
- webmFull Size VP8 Video (28 MB)
- ogvFull Size Theora Video (21.6 MB)
Nel 2007 con una serie di episodi avevo cercato di occuparmi della creazione di form complessi per gestire più modelli in un singolo form. Oggi quella serie può essere considerata obsoleta per cui a cominciare da questo episodio vi mostrerò alcune nuove tecniche per la costruzione di form con più modelli annidati.
La principale differenza rispetto al passato, del nostro nuovo approccio al problema è costituita dal metodo accepts_nested_attributes_for
aggiunto a partire da Rails 2.3. Nel corso di questa serie di episodi ne faremo uso e perciò è necessario che abbiate installato l'ultima versione di Rails disponibile al momento.
Vale la pena spendere un pò di tempo a leggere la documentazione per il metodo accepts_nested_attributes_for
che spiega come usarlo con attributi annidati in una singola chiamata di update. Però, risulta meno chiaro come si fa ad implementarla nella view stessa e pertanto nelle spiegazioni che seguono ci concentreremo su questo.
La nostra applicazione: Sondaggio (Survey)
Negli episodi di questa serie costruiremo un'applicazione per la raccolta di sondaggi on line. L'applicazione sarà dotata di un form complesso per la creazione e la modifica di sondaggi, grazie al quale sarà possibile inserirne il nome ed un certo numero di domande. Ogni domanda sarà munita di risposta multipla. Tale form avrà anche dei links per aggiungere e rimuovere dinamicamente domande e risposte.
Il form complesso per la creazione e la modifica di sondaggi on line.
Siamo in presenza di un'associazione tra modelli, profondamente annidata: in essa un sondaggio può avere molte domande e queste possono avere a loro volta molte risposte. Prima d'ora, con il metodo della serie di episodi precedente, non era possibile costruire facilmente forms così complessi; con l'avvento di Rails 2.3 è diventato molto facile.
Iniziamo
Costruiremo la nostra applicazione a partire da zero per cui il primo passo consiste nel creare un nuovo progetto Rails chiamato surveysays
.
rails surveysays
Per facilitare lo sviluppo faremo uso dei nifty generators di Ryan Bates’ . Useremo il nifty layout generator per creare il layout dell'applicazione.
script/generate nifty_layout
La nostra applicazione avrà tre modelli: Survey
, Question
e Answer
. Cominciamo costruendo lo scaffold per il modello Survey
con il nifty scaffold generator. Il modello Survey
avrà un solo attributo chiamato name
.
script/generate nifty_scaffold survey name:string
Adesso lanciamo la migrazione e creiamo la tabella surveys nel database.
rake db:migrate
A questo punto abbiamo lo scaffold necessario a elencare, creare e modificare sondaggi (surveys) e un form semplice che sarà la base per costruire quello più complesso.
Quello che vogliamo nel form sono campi che ci consentano di aggiungere domande e riposte quando creiamo un nuovo sondaggio (survey). Il primo passo da fare è la generazione del modello Question
(domanda). Questo avrà un campo survey_id
ed un campo content che conterrà il testo della domanda.
script/generate model question survey_id:integer content:text
Fatto questo migriamo di nuovo il database per creare la tabella questions
.
rake db:migrate
E’ poi necessario impostare le relazioni tra Survey
e Question
nei rispettivi model files.
/app/models/question.rb
class Question < ActiveRecord::Base belongs_to :survey end
/app/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy end
Da notare che nel modello Survey
abbiamo usato :dependent => :destroy
così che quando cancelliamo un sondaggio vengano automaticamente cancellate anche tutte le sue domande.
Nel modello Survey
utilizzeremo anche accepts_nested_attributes_for
così da poter gestire le domande attraverso un Survey
. Utilizzando questo metodo possiamo creare, modificare e cancellare domande contestualmente alla modifica di un sondaggio.
/app/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions end
Creazione del Form
Avendo impostato tanto il modello Survey
che Question
possiamo ora occuparci del form del sondaggio. Quello che vogliamo qui ottenere è l’aggiunta al form dei campi di ogni domanda del sondaggio. A tal fine possiamo utilizzare il metodo fields_for
per gestire i campi associati in un form, passandogli il nome del modello associato e creando un form builder per ognuno dei record domanda associati. Il builder restituirà una label e una textarea per ogni domanda.
/app/views/survey/_form.html.erb
<% form_for @survey do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% f.fields_for :questions do |builder| %> <p> <%= builder.label :content, "Question" %><br /> <%= builder.text_area :content, :rows => 3 %> </p> <% end %> <p><%= f.submit "Submit" %></p> <% end %>
Ricaricando però il form nulla è cambiato. Questo accade perchè un nuovo sondaggio non ha nessuna domanda associata e quindi nessuno dei campi della domanda verrà visualizzato. L’obiettivo finale sarà quello di avere un link “Add Question” nel form ma per ora ci limiteremo a creare alcune domande nell'azione new del SurveyController
.
/app/controllers/surveys_controller.rb
def new @survey = Survey.new 3.times { @survey.questions.build } end
In questo modo aggiungeremo tre domande ad un nuovo sondaggio che potremo vedere nel form una volta ricaricata la pagina. Possiamo ora inserire il nome e le prime due domande e inviare un nuovo sondaggio.
All’invio del sondaggio verrà creato un nuovo record Survey
ma non vedremo le suo domande poichè non le stiamo visualizzando nella pagina. Per risolvere questo problema è sufficiente modificare la view show
del Survey
perchè mostri anche le domande del sondaggio.
/app/views/survey/show.html.erb
<% title "Survey" %> <p> <strong>Name:</strong> <%=h @survey.name %> </p> <ol> <% for question in @survey.questions %> <li><%= h question.content %></li> <% end %> </ol> <p> <%= link_to "Edit", edit_survey_path(@survey) %> | <%= link_to "Destroy", @survey, :confirm => 'Are you sure?', :method => :delete %> | <%= link_to "View All", surveys_path %> </p>
Ricaricando la pagina del sondaggio vedremo elencate anche le sue domande. Questo dimostra che all’aggiunta del sondaggio sono state salvate anche le sue domande.
Possiamo anche modificare il sondoaggio e le domande allo stesso tempo, tutte le modifiche verranno salvate quando si invia il form.
Nella figura qui sopra sono presenti tre domande anche se abbiamo inserito solo il testo delle prime due. Sarebbe meglio se le domande vuote fossero rimosse automaticamente prima del salvataggio delle modifiche al sondaggio. Il metodo accepts_nested_attributes_for
mette a disposizione l'opzione reject_if
proprio per questo scopo. reject_if
accetta in ingresso una funzione lambda
alla quale viene passato un hash di attributi. Ci serviremo di esso per negare il salvataggio di una domanda se il suo contenuto testuale (content
) è vuoto.
/app/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? } end
Se ora creiamo un nuovo sondaggio in cui su tre domande solo due hanno il testo, vedremo che la domanda vuota non apparirà nel sondaggio.
E se volessimo cancellare delle domande preesistenti quando si sta modificando il sondaggio? Nell'applicazione definitiva inseriremo dei link per eliminare le domande ma per ora useremo un'opzione più semplice: le checkbox. Nel partial che contiene il form del sondaggio aggiungeremo una checkbox e un'etichetta per la cancellazione domanda per domanda.
/app/views/survey/_form.html.erb
<% form_for @survey do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% f.fields_for :questions do |builder| %> <p> <%= builder.label :content, "Question" %><br /> <%= builder.text_area :content, :rows => 3 %> <%= builder.check_box :_destroy %> <%= builder.label :_destroy, "Remove Question" %> </p> <% end %> <p><%= f.submit "Submit" %></p> <% end %>
Il trucco in questo caso sta nel fatto che le checkbox hanno un nome: _destroy
. Se è presente la spunta (e quindi il valore della checkbox è true
), il record viene cancellato all'invio del form.
Per far si che questo funzioni abbiamo bisogno di abilitarlo nel accepts_nested_attributes_for
nel modello Survey
aggiungendo :allow_destroy => true
.
/apps/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true end
Ora ricaricando la pagina avremo anche una checkbox “Remove Question” per ogni domanda.
Se selezioniamo la checkbox “Remove Question” di una domanda e inviamo il form, quella domanda verrà rimossa.
Aggiungere Risposte
A questo punto le domande sono impostate esattamente come le vogliamo ma non le risposte. Creiamo dunque un modello Answer
e impostiamo il nesting. Prima di tutto generiamo il modello.
script/generate model answer question_id:integer content:string
Quindi migriamo nuovamente il database.
rake db:migrate
Impostiamo poi la relazione tra i modelli Answer
e Question
. Answer
apparterrà (belong_to
) a Question
.
/app/models/answer.rb
class Answer < ActiveRecord::Base belongs_to :question end
Dovremo poi usare accepts_nested_attributes_for
per il modello Question
come abbiamo fatto per il modello Survey
.
/app/models/question.rb
class Question < ActiveRecord::Base belongs_to :survey has_many :answers, :dependent => :destroy accepts_nested_attributes_for :answers, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true end
Nel form dovremo aggiungere i campi per le risposte ma il codice della view diventerebbe illegibile si vi aggiungessimo il nesting di un altro modello. In seguito aggiungeremo le domande al form tramite un link JavaScript per entrambi questi motivi sposteremo il codice che mostra ogni domanda in un partial che chiameremo question_fields
.
Dopo aver fatto questo la view nel form sarà come segue:
/app/views/surveys/_form.html.erb
<% 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><%= f.submit "Submit" %></p> <% end %>
Da notare che passiamo il nome del partial da mostrare come una semplice stringa, facedo uso della nuova forma abbreviata introdotta in Rails 2.3. Passiamo anche il builder
al partial con il nome di f
. Nel nuovo partial question_fields
possiamo così utilizzare la variabile f
per visualizzare nel form gli elementi di una domanda Question
.
/app/views/surveys/_question_fields.html.erb
<p> <%= f.label :content, "Question" %><br /> <%= f.text_area :content, :rows => 3 %><br /> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove Question" %> </p>
Possiamo gestire i campi delle risposte in maniera simile e metterli nel loro partial. Nel partial _question_fields
richiederemo tutte le risposte della domanda e visualizzeremo un nuovo partial chiamato _answer_fields
.
/app/views/surveys/_question_fields.html.erb
<p> <%= f.label :content, "Question" %><br /> <%= f.text_area :content, :rows => 3 %><br /> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove Question" %> </p> <% f.fields_for :answers do |builder| %> <%= render 'answer_fields', :f => builder %> <% end %>
Nel nostro nuovo partial _answer_fields
inseriremo il codice per visualizzare un’Answer
.
/app/views/survey/_answer_fields.html.erb
<p> <%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %> </p>
Per poter vedere i campi delle risposte nel form dobbiamo anche modificare l’azione new
del SurveyController
così che le tre nuove domande che vengono create abbiano ognuna quattro risposte.
/app/controllers/survey_controller.rb
def new @survey = Survey.new 3.times do question = @survey.questions.build 4.times { question.answers.build } end end
Ora quando creiamo un nuovo sondaggio abbiamo tre domande ognuna con quattro risposte.
Quando riempiamo i campi e inviamo il form le risposte non vengono ancora visualizzate ma è facile risolvere questo problema. Nella view show
del sondaggio, nella sezione del codice che visualizza ogni domanda aggiungiamo del codice che visualizzi le risposte per ogni domanda.
/app/views/survey/show.html.erb
<ol> <% for question in @survey.questions %> <li><%= h question.content %></li> <ul> <% for answer in question.answers %> <li><%= h answer.content %></li> <% end %> </ul> <% end %> </ol>
Se ora aggiorniamo la pagina del sondaggio mostrerà tanto le domande quanto le risposte.
Non abbiamo ancora finito perchè vogliamo essere in grado di aggiungere e rimuovere le domande e le risposte nel form in maniera dinamica attraverso dei link. Mostreremo come fare questo nel prossimo episodio.