4 votes

Utilisation des modèles Rails avec accepts_nested_attributes_for

J'écris un modèle Rails simple appelé Personne qui has_many :phone_numbers et j'essaie d'enregistrer les numéros de téléphone dans un formulaire complexe sans avoir à écrire manuellement des méthodes setter. accepts_nested_attributes_for devrait faire ce que je veux mais j'ai du mal à le faire fonctionner. Voici le code que j'ai jusqu'à présent :

Migration

class CreatePeople < ActiveRecord::Migration
  def self.up
    create_table :people do |t|
      t.string :first_name
      t.string :last_name
      t.integer :address_id      
      t.string :email

      t.timestamps
    end
  end

  def self.down
    drop_table :people
  end
end

class CreatePhoneNumbers < ActiveRecord::Migration
  def self.up
    create_table :phone_numbers do |t|
      t.string :number, :limit => 10
      t.string :extension, :limit => 5
      t.string :description, :null => false
      t.integer :telephone_id
      t.string :telephone_type

      t.timestamps
    end
  end

  def self.down
    drop_table :phone_numbers
  end
end

Modèles

class Person < ActiveRecord::Base
  has_one :address, :as => :addressable, :dependent => :destroy
  has_many :phone_numbers,
    :as => :telephone,
    :dependent => :destroy

  accepts_nested_attributes_for :phone_numbers

  attr_protected :id

  validates_presence_of :first_name, :last_name, :email
end

class PhoneNumber < ActiveRecord::Base
  attr_protected :id

  belongs_to :telephone, :polymorphic => true
end

Voir

<% form_for @person, :builder => CustomFormBuilder do |f| %>
  <%= f.error_messages %>

  <%= f.text_field :first_name %>
  <%= f.text_field :last_name %>

  <% fields_for "person[address]", @person.address, :builder => CustomFormBuilder do |ff| %>  
    <%= ff.text_field :address_1 %>
    <%= ff.text_field :address_2 %>
    <%= ff.text_field :city %>
    <%= ff.text_field :state %>
    <%= ff.text_field :zip %>  
  <% end %>

  <h2>Phone Numbers</h2>
  <% @person.phone_numbers.each do |phone_number| %>
    <% fields_for "person[phone_numbers][]", phone_number, :builder => CustomFormBuilder do |ff| %>
      <%= ff.text_field :description %>
      <%= ff.text_field :number %>
      <%= ff.text_field :extension %>
    <% end %>
  <% end %>

  <%= f.text_field :email %>

  <%= f.submit 'Create' %>

<% end %>

Contrôleur

def new
  @person = Person.new 
  @person.build_address
  @person.phone_numbers.build

  respond_to { |format| format.html }
end

def create
  @person = Person.new(params[:person])

  respond_to do |format|
    if @person.save
      flash[:notice] = "#{@person.name} was successfully created."
      format.html { redirect_to(@person) }
    else
      format.html { render :action => 'new' }
    end
  end
end

J'ai vérifié qu'une méthode phone_numbers= est créée, mais le post cause toujours :

PhoneNumber(#69088460) expected, got HashWithIndifferentAccess(#32603050)

RAILS_ROOT: H:/projects/test_project

C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_proxy.rb:263:in `raise_on_type_mismatch'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_collection.rb:319:in `replace'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_collection.rb:319:in `each'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_collection.rb:319:in `replace'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations.rb:1290:in `phone_numbers='
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2740:in `send'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2740:in `attributes='
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2736:in `each'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2736:in `attributes='
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2434:in `initialize'
H:/projects/salesguide/app/controllers/accounts_controller.rb:46:in `new'
H:/projects/test_project/app/controllers/accounts_controller.rb:46:in `create'

Je peux faire en sorte que cela fonctionne en écrivant manuellement la méthode phone_numbers=, mais cela entraînerait une énorme duplication des efforts, je préférerais apprendre à faire cela correctement. Quelqu'un peut-il voir ce que je fais mal ?

6voto

EmFi Points 18645

Vous oubliez d'appeler fields_for comme une méthode sur le formulaire de la personne. Sinon, vous n'utilisez pas réellement fields_for dans un contexte accept_nested_attributes_for. La solution de Michael tente d'inciter Rails à traiter la soumission comme un formulaire accept_nested_attributes_for correctement défini.

La syntaxe correcte pour ce que vous essayez de faire est :

parent_form_object.fields_for id, object_containing_values, {form_for options}, &block

Vous constaterez que le code est plus propre et plus simple à déboguer si vous fournissez un symbole comme id, contenant le nom d'association du modèle enfant tel que défini dans votre modèle Personne.

En outre, le bloc each que vous utilisez peut poser des problèmes si @person.phone_numbers est vide. Vous pouvez vous assurer qu'il y a au moins un ensemble de champs Numéro de téléphone avec une ligne similaire à celle que j'ai utilisée avec

<% @phs = @person.phone_numbers.empty? ? @person.phone_numbers.build : @person.phone_numbers %> 

Avec toutes les corrections, ce code fera ce que vous voulez qu'il fasse.

Voir

<% form_for @person, :builder => CustomFormBuilder do |f| %>
  <%= f.error_messages %>

  <%= f.text_field :first_name %>
  <%= f.text_field :last_name %>

  <% f.fields_for :address, @person.address, :builder => CustomFormBuilder do |address_form| %>  
    <%= address_form.text_field :address_1 %>
    <%= address_form.text_field :address_2 %>
    <%= address_form.text_field :city %>
    <%= address_form.text_field :state %>
    <%= address_form.text_field :zip %>  
  <% end %>

  <h2>Phone Numbers</h2>
    <% @phs = @person.phone_numbers.empty? ? @person.phone_numbers.build : @person.phone_numbers %>
    <% f.fields_for :phone_numbers, @phs, :builder => CustomFormBuilder do |phone_number_form| %>
      <%= phone_number_form.text_field :description %>
      <%= phone_number_form.text_field :number %>
      <%= phone_number_form.text_field :extension %>
    <% end %>

  <%= f.text_field :email %>

  <%= f.submit 'Create' %>

<% end %>

Vous pourriez trouver utile de consulter le dépôt complex-form-examples sur github pour un exemple concret. Il est également accompagné d'un code permettant d'ajouter dynamiquement de nouvelles entrées pour les relations :has_many à partir de la vue/du formulaire.

4voto

Michael Sepcot Points 5455

Je jouais avec accepts_nested_attributes_for hier en essayant de comprendre Formulaire Rails avec trois modèles et un espace de noms . J'ai dû configurer le formulaire de manière légèrement différente, essayez d'utiliser : person[phone_numbers_attributes][]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X