Поскольку никто не предложил ответа на этот вопрос, даже после щедрости, мне наконец-то удалось заставить это работать самостоятельно. Это не должно было быть глупее! Надеюсь, это будет проще сделать в Rails 3.0.
Пример Энди - это хороший способ удаления записей напрямую, без отправки формы на сервер. В данном конкретном случае мне действительно нужен способ динамического добавления / удаления полей перед обновлением вложенной формы. Это немного другой случай, потому что, поскольку поля удаляются, они фактически не удаляются, пока форма не будет отправлена. Я, вероятно, в конечном итоге буду использовать оба в зависимости от ситуации.
Моя реализация основана на примерах сложных форм Тима Райли fork на github.
Сначала настройте модели и убедитесь, что они поддерживают вложенные атрибуты:
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
Создание частичного представления для полей формы PhoneNumber:
<div class="fields">
<%= f.text_field :description %>
<%= f.text_field :number %>
</div>
Далее напишите базовый вид редактирования для модели Person:
<% 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 -%>
Это будет работать путем создания набора полей шаблона для модели PhoneNumber, которые мы можем дублировать с помощью javascript. Мы создадим вспомогательные методы в app/helpers/application_helper.rb
для этого:
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
Теперь добавьте эти вспомогательные методы в часть редактирования:
<% 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 -%>
Теперь у вас есть готовые шаблоны js. Он будет представлять пустой шаблон для каждой ассоциации, но предложение :reject_if
в модели будет отбрасывать их, оставляя только созданные пользователем поля. Обновление: Я переосмыслил этот дизайн, см. Ниже.
Это на самом деле не AJAX, поскольку на сервере не происходит никакой связи, кроме загрузки страницы и отправки формы, но я, честно говоря, не смог найти способ сделать это после этого.
На самом деле это может обеспечить лучшее взаимодействие с пользователем, чем AJAX, поскольку вам не нужно ждать ответа сервера для каждого дополнительного поля, пока вы не закончите.
Наконец, нам нужно связать это с JavaScript. Добавьте следующее в ваш файл `public / javascripts / application.js ':
$(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;
});
});
К этому времени вы должны иметь динамическую форму barebones! Javascript здесь действительно прост, и его можно легко сделать с помощью других фреймворков. Вы можете легко заменить мой application.js
код, например, на prototype + lowpro. Основная идея заключается в том, что вы не встраиваете гигантские функции JavaScript в свою разметку, и вам не нужно писать утомительные функции phone_numbers=()
в ваших моделях. Все просто работает. Ура!
После некоторого дальнейшего тестирования я пришел к выводу, что шаблоны должны быть удалены из полей <form>
. Хранение их там означает, что они отправляются обратно на сервер с остальной частью формы, и это только позже вызывает головную боль.
Я добавил это в конец моего макета:
<div id="jstemplates">
<%= yield :jstemplates %>
</div
И изменил хелпер new_child_fields_template
:
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
Теперь вы можете удалить предложения :reject_if
из ваших моделей и перестать беспокоиться о отправке шаблонов назад.