#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)
En 2007, une série d’épisodes couvrait la création de formulaires complexes, qui pouvaient gérer plusieurs modèles dans le même formulaire. Cette série est maintenant un peu dépassée et à présent, on va aborder des techniques plus modernes pour gérer des formulaires incluant plusieurs modèles.
Une des différences majeures dans l’approche de ce problème est la méthode accepts_nested_attributes_for, qui a été ajoutée à Rails 2.3. Nous allons l’utiliser tout au long de cette série, si bien qu’il vous faudra une version de Rails récente (>2.3) pour que vous puissiez utiliser cette technique dans votre propre application.
La documentation est intéressante à lire et montre bien comment l’utiliser avec des attributs imbriqués sur une simple mise à jour, mais cela devient moins évident lorsqu’il faut l’utiliser au niveau de la vue elle-même. C’est ce que nous allons voir.
Notre application ‘Survey’
Tout d’abord, l’objectif de cette application a pour but de gérer des enquêtes (surveys). Cette application comportera un formulaire (complexe) pour créer et modifier des enquêtes, nous permettant d’entrer un nom pour l’enquête, ainsi qu’un certain nombre de questions et chaque question pouvant avoir plusieurs réponses. Le formulaire comportera aussi des liens pour ajouter ou retirer dynamiquement des questions et des réponses à partir d’une même enquête.
Le formulaire pour créer et modifier les enquêtes.
Ce que nous avons ici est une association fortement imbriquée, dans laquelle une enquête a plusieurs questions, qui ont elles-mêmes plusieurs réponses. Dans les épisodes précédents sur les formulaires complexes, il n’était pas possible de créer des formulaires avec un tel type de relation, mais maintenant, depuis Rails 2.3, c’est possible.
Pour commencer
Nous allons démarrer de zéro cette application d’enquêtes et nous commencerons donc par créer une application Rails que nous appellerons: surveysays
.
<p>Pour simplifer l’écriture, on utilisera 2 des <a href="http://github.com/ryanb/nifty-generators">générateurs de Ryan Bates</a> et on commencera par nifty_layout pour générer un modèle de page (layout) pour toute l’application.</p> ``` terminal script/generate nifty_layout
Notre application comportera 3 modèles: Survey
(Enquête), Question
, Answer
(Réponse). Commençons par les enquêtes et utilisons le générateur nifty_scaffold pour créer un scaffold. Survey aura un seul attribut, son nom (name).
<p>Ensuite lançons la migration pour créer la table des enquêtes dans la base de données.</p> ``` terminal rake db:migrate
Si on regarde de plus près les fichiers générés par le scaffold, on peut à présent lister, créer et modifier les enquêtes à travers un formulaire basique sur lequel nous allons nous appuyer.
La première étape est de créer le modèle Question
des questions. Il incluera un champ survey_id
et un champ contenu
(content) référent au texte de la question.
Ceci étant fait, migrons une nouvelle fois pour créer la table questions.
L’étape suivante est de créer la relation entre Survey
et Question
dans leur modèle respectif.
``` ruby class Question < ActiveRecord::Base belongs_to :survey end
``` ruby class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy end
Notez que dans le modèle Survey
nous avons utilisé :dependent => :destroy
de telle sorte que si on supprime une enquête, les questions le seront aussi.
Dans le modèle Survey
nous allons utiliser accepts_nested_attributes_for
afin de gérer les questions à travers le modèle Survey
. Ce faisant, nous pouvons créer, modifier ou détruire des questions lorsque nous mettons à jour un attribut d’une enquête.
``` ruby class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions end
Création du formulaire
Maintenant que les modèles Survey
et Question
sont bien établis, nous pouvons pousuivre avec le formulaire des enquêtes. Ce que nous voulons faire à présent, c’est d’ajouter dans les champs pour chaque question de l’enquête. Nous pouvons utiliser la méthodefields_for
pour gérer les champs associés. Le générateur va rendre un label et une zone de texte pour chaque question.
``` ruby <% 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 %>
Lorsque nous rechargeons le formulaire, il ressemble à ce qu’il était auparavant. Parce qu’une nouvelle enquête ne possède aucune question associée, cela n’affichera aucun champ question. Au final, nous voulons avoir un lien “Add Question” sur le formulaire, mais pour l’instant nous allons créer seulement quelques questions avec l’action new du SurveyController
.
``` ruby def new @survey = Survey.new 3.times { @survey.questions.build } end
Ceci va générer 3 questions pour une nouvelle enquête, que nous voyons apparaître lorsque nous rechargeons la page. Remplissons maintenant le nom et les 2 premières questions puis souemttons la nouvelle enquête.
Lorsque nous soumettons l’enquête, un nouvel enregistrement Survey
va être créé, mais nous ne pourrons voir ses questions , car elles ne sont pas affichées sur la page. Pour régler cela, on va modifier la vu de show
pour Survey
pour afficher les questions de l’enquête.
``` ruby <% 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>
Lorque nous rechargeons la page de l’enquête, nous allons voir les questions, ce qui prouve bien qu’une fois l’enquête enregistrée, les questions le sont bien aussi.
On peut aussi éditer une enquête et si on change n’importe quelle question, elles seront toutes mises à jour lorsque nous allons soumettre le formulaire.
Sur la page ci-dessus, trois questions s’affichent, même si on n’a rentré des valeurs que pour deux. Ce serait bien mieux si les questions vides pouvaient être automatiquement retirées. La méthode accepts_nested_attributes_for
a une option reject_if
que l’on va utiliser pour cela. Cette méthode accepte un lambda
qui prend un hash d’attributs en paramètre et on va utiliser ce hash to rejeter une question si content
est vide.
``` ruby class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? } end
Si on crée maintenant une nouvelle enquête avec seulement deux champs questions remplis, uniquement 2 questions seront créées et on ne verra pas de question vide s’afficher.
Comment faire si on veut supprimer des questions existantes lorsqu’on est en train de modifier une enquête? Dans l’application finale, on veut utiliser un lien pour supprimer les questions, mais pour l’instant nous allons au plus facile et simplement mettre un checkbox. Dans le partial _form, on ajoute un checkbox et un label qui va de pair.
``` codeFilePath /app/views/survey/_form.html.erb``` ruby <% 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 %>
L’astuce ici est dans le nom de l’attribut pour la checkbox: _destroy
. Quand il a une valeur de true
, c-a-d quand le checkbox est coché, L’enregistrement sera supprimé lorsque le formulaire sera soumis.
Afin que tout ceci fontionne, on doit l’activer à travers accepts_nested_attributes_for
dans le modèle Survey
en ajoutant :allow_destroy => true
.
``` ruby 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
Lorqu’on recharge la page, on a à présent un checkbox “Remove Question” à côté de chaque question.
Si on coche le checkbox “Remove Question” en face d’une des questions et que l’on soumet le formulaire, cette question sera supprimée.
Ajout de questions
Les questions sont maintenant définies comme on voulait mais pas les réponses. On commence donc par créer le modèle Answer
et son imbrication. Premièrement on va générer le modèle.
<p>Ensuite on migre de nouveau la base de données.</p> ``` terminal rake db:migrate
Puis on crée la relation entre Answer
et Question
. Answer
définit belong_to
Question
.
``` ruby class Answer < ActiveRecord::Base belongs_to :question end
Pour Question
il va falloir utiliser accepts_nested_attributes_for
de la même façon que cela avait été fait pour Survey
.
``` ruby 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
Dans le formulaire on aura besoin de champs pour les réponses, mais le code du form view va vite devenir fouilli si on lui rajoute un autre modèle imbriqué. Plus tard, on voudra ajouter des questions sur le formulaire via un lien avec du JavaScript, et pour ces deux raisons, on va placer le code responsable de chaque question dans un partial appelé question_fields
.
Le form view devrait ressembler à cela.
``` codeFilePath /app/views/surveys/_form.html.erb``` ruby <% 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 %>
Notez qu’on lui passe juste le nom du partial en tant que string, c’est un raccourci qui a été introduit dans Rails 2.3. On lui passe aussi le builder
ayant pour nom f
. Dans le nouveau partial question_fields
on pourra alors utiliser la variable f
pour le rendu des éléments du formulaire du modèle Question
.
``` ruby <p> <%= f.label :content, "Question" %><br /> <%= f.text_area :content, :rows => 3 %><br /> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove Question" %> </p>
On peut gérer les champs réponses de la même manière et les mettre dans leur propre partial. Dans le partial _question_fields
on va parcourir toutes les réponses de la question and générer un nouveau partial que l’on appellera _answer_fields
.
``` ruby <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 %>
Dans notre nouveau partial _answer_fields
on va placer le code pour afficher une Answer
.
``` ruby <p> <%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %> </p>
Maintenant que l’on voit les champs réponses, on va modifier l’action new
du SurveyController
afin que les 3 nouvelles questions ainsi créées aient chacune 4 réponses.
``` ruby def new @survey = Survey.new 3.times do question = @survey.questions.build 4.times { question.answers.build } end end
A présent, lorsqu’on crée une enquête, on a 3 questions avec chacune 4 réponses.
Quand on remplit les champs et que l’on soumet le formulaire, les réponses ne vont pas s’afficher, mais on peut facilement y remédier. Dans la partie de la vue show
pour une enquête qui affiche chaque question, on va rajouter un peu de code pour afficher les réponses à chaque question.
``` ruby <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>
Si maintenant on recharge la page de l’enquête, on verra les questions et les réponses.
On n’a pas encore terminé, dans la mesure où on veut être capable d’ajouter ou supprimer des questions ou des réponses dynamiquement sur le formulaire grâce à des liens. C’est ce que nous aborderons dans le prochain épisode.