57 votes

Comment soumettre plusieurs NOUVEAUX articles via l'affectation en masse de Rails 3.2

J'ai un assez standard de cas d'utilisation. J'ai un parent de l'objet et une liste des objets enfants. Je veux avoir la forme de tableaux où je peux modifier tous les enfants à la fois, comme les lignes de la table. Je veux aussi être capable d'insérer une ou plusieurs nouvelles lignes, et à soumettre les avoir être créés en tant que de nouveaux enregistrements.

Lorsque j'utilise un fields_for de rendre une série de sous-formulaires pour les sous dossiers liés par a-beaucoup, rails génère des noms de champ, par exemple, parent[children_attributes][0][fieldname], parent[children_attributes][1][fieldname] et ainsi de suite.

Cela provoque grille pour analyser un params de hachage qui ressemble à ceci:

{ "parent" => { 
    "children" => {
      "0" => { ... },
      "1" => { ... } } }

Quand a adopté une nouvelle (non persistants) de l'objet, le même fields_for va générer un nom de champ qui ressemble à:

parent[children_attributes][][fieldname]

Remarque l' [] sans index.

Ce ne peut pas être affiché dans le même formulaire avec les champs contenant [0], [1], etc. parce Rack devient confus et pose

TypeError: expected Array (got Rack::Utils::KeySpaceConstrainedParams)

"OK", se dit I. "je vais juste assurez-vous que tous les champs de l'utilisation de l' [] formulaire au lieu de l' [index] formulaire. Mais je ne peux pas comprendre comment convaincre fields_for à le faire de manière cohérente. Même si je donne explicitement le champ préfixe de nom et d'objet:

fields_for 'parent[children_attributes][]', child do |f| ...

Tant que child est persistant, il va automatiquement modifier le fieldnames, afin qu'ils deviennent par exemple, parent[children_attributes][0][fieldname], tout en laissant fieldnames pour les nouveaux enregistrements parent[children_attributes][][fieldname]. Une fois de plus, Rack barfs.

Je suis à une perte. Comment diable puis-je utiliser les Rails aides comme fields_for présenter plusieurs nouveaux dossiers, ainsi que des enregistrements existants, demandez-leur d'être analysée comme un tableau dans les paramètres, et de tous les enregistrements de défaut Id être créés en tant que de nouveaux enregistrements dans la base de données? Je suis hors de la chance et je n'ai qu'à générer tous les noms de champ manuellement?

45voto

ryanb Points 11043

Comme d'autres l'ont mentionné, l' [] doit contenir une clé pour de nouveaux enregistrements parce que sinon c'est le mélange d'un hash avec un type tableau. Vous pouvez définir ceci avec l' child_index option sur fields_for.

f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ...

J'ai l'habitude de le faire à l'aide de l' object_id au lieu de s'assurer qu'il est unique dans le cas où il ya plusieurs nouveaux éléments.

item = Item.new
f.fields_for :items, item, child_index: item.object_id # ...

Voici un résumé de la méthode d'assistance que ce n'. Cela suppose qu'il existe une partielle avec le nom de l' item_fields qu'il rendra.

def link_to_add_fields(name, f, association)
  new_object = f.object.send(association).klass.new
  id = new_object.object_id
  fields = f.fields_for(association, new_object, child_index: id) do |builder|
    render(association.to_s.singularize + "_fields", f: builder)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

Vous pouvez l'utiliser comme ça. Les arguments sont: le nom du lien, du parent, générateur de formulaire, et le nom de l'association sur le modèle parent.

<%= link_to_add_fields "Add Item", f, :items %>

Et voici quelques CoffeeScript pour écouter l'événement click du lien, insérer les champs, et de mettre à jour l'id de l'objet à l'heure actuelle, pour lui donner une clé unique.

jQuery ->
  $('form').on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

Ce code est tiré de ce RailsCasts Pro épisode qui nécessite un abonnement payant. Cependant, il y a un exemple de travail librement disponible sur GitHub.

Mise à jour: je tiens à souligner que l'insertion d'un child_index de l'espace réservé n'est pas toujours nécessaire. Si vous ne souhaitez pas utiliser JavaScript pour insérer de nouveaux enregistrements de manière dynamique, vous pouvez les construire à l'avance:

def new
  @project = Project.new
  3.times { @project.items.build }
end

<%= f.fields_for :items do |builder| %>

Rails pour insérer automatiquement un index pour les nouveaux enregistrements, afin de il devrait fonctionner.

23voto

Avdi Points 13086

Donc, je n'étais pas heureux avec la solution que j'ai vu le plus souvent, qui était de générer un pseudo-indice pour les nouveaux éléments, que ce soit sur le serveur ou côté client JS. Cela se sent comme une bidouille, surtout à la lumière du fait que les Rails/Rack est parfaitement capable de l'analyse des listes d'éléments, aussi longtemps qu'ils utilisent tous des parenthèses vides ([]) que celle de l'indice. Voici une approximation du code, je me suis retrouvé avec:

# note that this is NOT f.fields_for.
fields_for 'parent[children_attributes][]', child, index: nil do |f|
  f.label :name
  f.text_field :name
  # ...
end

En terminant, le nom du champ préfixe [], couplé avec l' index: nil option désactive la génération index Rails donc utilement cherche à fournir pour les objets persistants. Cet extrait de code qui fonctionne pour les deux nouveaux et les objets sauvés. Le formulaire des paramètres, car ils utilisent de manière constante [], sont analysées dans un tableau dans l' params:

params[:parent][:children_attributes] # => [{"name" => "..."}, {...}]

L' Parent#children_attributes= méthode générée par accepts_nested_attributes_for :children traite de ce tableau d'amende, la mise à jour des enregistrements modifiés, en ajouter de nouveaux (ceux qui n'ont pas la "id" - clés), et la suppression de ceux avec l' "_destroy" jeu de clés.

Je suis toujours gêné que les Rails qui rend ce si difficile, et que j'ai dû revenir à un codée en dur dans le champ préfixe de nom de chaîne au lieu d'utiliser par exemple, f.fields_for :children, index: nil. Pour l'enregistrement, même en procédant de la manière suivante:

f.fields_for :children, index: nil, child_index: nil do |f| ...

...ne parvient pas à désactiver le champ de l'indice de génération.

J'envisage l'écriture d'un Rails de patch pour rendre cela plus facile, mais je ne sais pas si suffisamment de gens de soins ou si elle serait même accepté.

EDIT: Utilisateur @Macario a clued sur moi pour pourquoi Rails préfère explicite des indices dans les noms de champ: une fois que vous obtenez dans les trois couches de modèles imbriqués, il doit y avoir un moyen de discrimination dont le second modèle d'un troisième niveau de l'attribut appartient.

14voto

julian7 Points 151

La solution courante consiste à ajouter un espace réservé dans [] et à le remplacer par un numéro unique lors de l'insertion de l'extrait de code dans le formulaire. L'horodatage fonctionne la plupart du temps.

3voto

Josh Susser Points 31

Peut-être devriez-vous simplement tricher. Mettez les nouveaux disques dans un faux attribut différent qui est un décorateur pour le réel.

 parent[children_attributes][0][fieldname]
parent[new_children_attributes][][fieldname]
 

Ce n'est pas joli, mais ça devrait marcher. Des efforts supplémentaires peuvent être nécessaires pour prendre en charge les allers-retours au formulaire en cas d'erreur de validation.

3voto

Sharagoz Points 2126

long post supprimé

Ryan a un épisode à ce sujet: http://railscasts.com/episodes/196-nested-model-form-revised

Il semble que vous deviez générer l'index unique manuellement. Ryan utilise les object_id pour cela.

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