Il n’est pas toujours aisé de traiter les données envoyées par un formulaire. Souvent, les contrôleurs grossissent lorsque les formulaires se complexifient. Avec fields_for, il est possible de transférer tout ce traitement dans le modèle. Nos contrôleurs restent alors parfaitement DRY.
Le problème
Prenons l’exemple habituel du blog.
1 class Post < ActiveRecord::Base 2 belongs_to :author 3 end 4 5 class Author < ActiveRecord::Base 6 has_many :posts 7 end
Chaque Post est écrit par un Author. Dans le formulaire de création de Post, les champs name et email de l’auteur sont inclus.
Nous voudrions créer à la volée un enregistrement Author dans la base de données au moment de la création de Post.
Pour le moment, notre application ressemble à ceci.
Vue du formulaire (app/view/posts/_form.html.erb) :
1 <p> <b>Title</b><br /> <%= f.text_field :title %> </p> 2 3 <p> <b>Content</b><br /> <%= f.text_area :content %> </p> 4 5 <p> <b>Name</b><br /> <%= text_field_tag :name, @post.author.name %> </p> 6 7 <p> <b>Email</b><br /> <%= text_field_tag :email, @post.author.email %> </p>
Action create dans PostsController (app/controllers/posts_controller.rb) :
1 def create 2 @post = Post.new(params[:post]) 3 @post.author = Author.new(:name => params[:name], :email => params[:email]) 4 5 respond_to do |format| 6 if @post.save 7 # response code 8 end 9 end 10 end
La solution
Nous désirons simplifier la vue et le contrôleur. La principale cause de désagrément est l’organisation des attributs envoyés par le formulaire :
{ "name"=>"Jean-Baptiste Escoyez",
"email"=>"jbe at belighted point com",
"post"=>{
"title"=>"Editer plusieurs modèles avec fields_for",
"content"=>"Contenu..."}}
Nous voyons que name et email ne sont pas associés à post. De ce fait, nous sommes obligés de créer un Author dans le contrôleur.
Nous utilisons fields_for pour nous aider à organiser les arguments.
Vue du formulaire (app/view/posts/_form.html.erb):
1 <p> <b>Title</b><br /> <%= f.text_field :title %> </p> 2 3 <p> <b>Content</b><br /> <%= f.text_area :content %> </p> 4 5 <% fields_for :author, @post.author do |af| %> 6 <p> <b>Name</b><br /> <%= af.text_field :name %> </p> 7 8 <p> <b>Email</b><br /> <%= af.text_field :email %> </p> 9 <% end %>
Action create dans PostsController (app/controllers/posts_controller.rb):
1 def create 2 @post = Post.new(params[:post]) 3 @post.author = Author.new(params[:author]) 4 5 respond_to do |format| 6 if @post.save 7 # response code 8 end 9 end 10 end
Remarquez le second argument de fields_for. Il s’agit de l’objet qui donnera les valeurs aux champs name et email au moment de l’édition du post. Le contrôleur a également changé car les arguments envoyés par le formulaire sont maintenant organisés différemment.
{ "author"=>{
"name"=>"Jean-Baptiste Escoyez",
"email"=>"jbe at belighted point com"},
"post"=>{
"title"=>"Editer plusieurs modèles avec fields_for",
"content"=>"Contenu..."} }
Encore mieux !
Nous pouvons aller encore plus loin dans notre refactorisation. En changeant fields_for par f.fields_for, vous allez imbriquer les attributs des deux objets.
{ "post"=>{
"name"=>"Jean-Baptiste Escoyez",
"email"=>"jbe at belighted point com",
"author"=>{
"title"=>"Editer plusieurs modèles avec fields_for",
"content"=>"Contenu..."} } }
Comme une des clefs du hash est author, la méthode author= de post sera appelée lorsque vous appellerez Post.new(params[:post]). Malheureusement, la méthode author n’est pas prévue pour prendre un hash en paramètre mais un objet Author. Il faut donc remplacer f.fields_for :author par f.fields_for :author_attributes et créer une méthode author_attributes= dans le modèle Post ; comme la méthode author_attributes= est semblable à build_author, un alias de méthode suffit:
1 class Post < ActiveRecord::Base 2 belongs_to :author 3 alias author_attributes= build_author 4 end
Vous pouvez dès lors supprimer la ligne post.author = Author.new(params[:author]) dans votre contrôleur.
1 def create 2 @post = Post.new(params[:post]) 3 @post.author = Author.new(params[:author]) 4 5 respond_to do |format| 6 if @post.save 7 # response code 8 end 9 end 10 end
fields_for peut être utilisé de manière imbriquée si vous avez plusieurs modèles à créer/éditer. N’oubliez pas le f. avant fields_for si vous voulez imbriquer les attributs.
fields_for peut également être utilisé sans modèle correspondant, juste pour structurer l’information envoyée par un formulaire. Depuis que j’utilise fields_for, j’ai complètement laissé tomber les FormTagHelper.