Puisque personne n'a offert une réponse, même après un bounty, j'ai enfin réussi à obtenir ce travail moi-même. Ce n'était pas censé être une stumper! J'espère que ce sera plus facile de le faire dans les Rails 3.0.
Andy exemple est un bon moyen de supprimer des enregistrements directement, sans l'envoi d'un formulaire vers le serveur. Dans ce cas particulier, ce que je suis vraiment à la recherche d'une façon dynamique d'ajouter/supprimer des champs avant de faire une mise à jour d'un formulaire imbriqué. C'est un cas un peu différent, parce que les champs sont supprimés, ils ne sont pas réellement supprimés jusqu'à ce que le formulaire est soumis. Je vais probablement jusqu'à la fin en utilisant à la fois en fonction de la situation.
J'ai basé mon application sur Tim Riley, complexe-formes-exemples fork sur github.
D'abord configurer les modèles, et assurez-vous qu'ils soutiennent imbriqués les attributs:
class Person < ActiveRecord::Base
has_many :phone_numbers, :dependent => :destroy
accepts_nested_attributes_for :phone_numbers, :reject_if => lambda { |p| p.values.all?(&:blank?) }, :allow_destroy => true
end
class PhoneNumber < ActiveRecord::Base
belongs_to :person
end
Créer une vue partielle pour le Numéro de champs de formulaire:
<div class="fields">
<%= f.text_field :description %>
<%= f.text_field :number %>
</div>
Prochaine écriture de base modifier la vue de la Personne modèle:
<% form_for @person, :builder => LabeledFormBuilder do |f| -%>
<%= f.text_field :name %>
<%= f.text_field :email %>
<% f.fields_for :phone_numbers do |ph| -%>
<%= render :partial => 'phone_number', :locals => { :f => ph } %>
<% end -%>
<%= f.submit "Save" %>
<% end -%>
Ce sera le travail par la création d'un ensemble de champs de modèle pour le Numéro de modèle que nous pouvons reproduire avec javascript. Nous allons créer des méthodes d'assistance en app/helpers/application_helper.rb
pour ce:
def new_child_fields_template(form_builder, association, options = {})
options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new
options[:partial] ||= association.to_s.singularize
options[:form_builder_local] ||= :f
content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do
form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f|
render(:partial => options[:partial], :locals => { options[:form_builder_local] => f })
end
end
end
def add_child_link(name, association)
link_to(name, "javascript:void(0)", :class => "add_child", :"data-association" => association)
end
def remove_child_link(name, f)
f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child")
end
Maintenant, ajoutez ces méthodes d'aide à la modification partielle:
<% form_for @person, :builder => LabeledFormBuilder do |f| -%>
<%= f.text_field :name %>
<%= f.text_field :email %>
<% f.fields_for :phone_numbers do |ph| -%>
<%= render :partial => 'phone_number', :locals => { :f => ph } %>
<% end -%>
<p><%= add_child_link "New Phone Number", :phone_numbers %></p>
<%= new_child_fields_template f, :phone_numbers %>
<%= f.submit "Save" %>
<% end -%>
Vous avez maintenant la js templating fait. Il présentera un modèle vierge pour chaque association, mais l' :reject_if
clause dans le modèle seront les ignorer, ne laissant que l'utilisateur créé des champs. Mise à jour: j'ai repensé à cette conception, voir ci-dessous.
Ce n'est pas vraiment AJAX, car il n'y a pas de communication d'aller sur le serveur-delà de la page de chargement et de soumettre le formulaire, mais honnêtement, je ne pouvais pas trouver un moyen de le faire après le fait.
En fait, cela peut fournir une meilleure expérience utilisateur que l'AJAX, puisque vous n'avez pas à attendre une réponse du serveur pour chaque champ supplémentaire jusqu'à ce que vous avez terminé.
Enfin, nous avons besoin de la relier avec javascript. Ajoutez les lignes suivantes à votre " public/javascripts/application.js fichier de:
$(function() {
$('form a.add_child').click(function() {
var association = $(this).attr('data-association');
var template = $('#' + association + '_fields_template').html();
var regexp = new RegExp('new_' + association, 'g');
var new_id = new Date().getTime();
$(this).parent().before(template.replace(regexp, new_id));
return false;
});
$('form a.remove_child').live('click', function() {
var hidden_field = $(this).prev('input[type=hidden]')[0];
if(hidden_field) {
hidden_field.value = '1';
}
$(this).parents('.fields').hide();
return false;
});
});
En ce moment, vous devriez avoir un barebones forme dynamique! Le javascript ici est vraiment simple, et pourrait facilement être fait avec les autres cadres. Vous pouvez facilement remplacer mon application.js
code du prototype + lowpro par exemple. L'idée de base est que vous n'êtes pas l'incorporation de gigantesques fonctions javascript dans votre balisage, et vous n'avez pas à écrire fastidieux phone_numbers=()
fonctions dans vos modèles. Tout fonctionne, tout simplement. Hourra!
Après quelques tests, j'en ai conclu que les modèles doivent être déplacés hors de l' <form>
champs. Les y maintenir signifie qu'ils sont envoyés au serveur avec le reste de la forme, et cela crée des maux de tête plus tard.
J'ai ajouté cette à la fond de ma présentation:
<div id="jstemplates">
<%= yield :jstemplates %>
</div
Et modifié le new_child_fields_template
helper:
def new_child_fields_template(form_builder, association, options = {})
options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new
options[:partial] ||= association.to_s.singularize
options[:form_builder_local] ||= :f
content_for :jstemplates do
content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do
form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f|
render(:partial => options[:partial], :locals => { options[:form_builder_local] => f })
end
end
end
end
Maintenant, vous pouvez supprimer l' :reject_if
clauses de vos modèles et de cesser de s'inquiéter sur les modèles d'être envoyé.