Если Вы пишете веб-приложение, удостоверяетесь, чтобы у Вас не было конфликтующих версий банки в глобальном каталоге библиотеки Вашего контейнера и также в Вашем приложении. Вы не можете обязательно знать, какая банка используется classloader.
, например,
Поскольку никто не предложил ответа на этот вопрос, даже после награды, мне наконец удалось заставить это работать самому. Это не должно было быть загадкой! Надеюсь, это будет проще сделать в Rails 3.0.
Пример Энди - хороший способ удаления записей напрямую, без отправки формы на сервер. В данном конкретном случае я действительно ищу способ динамического добавления / удаления полей перед обновлением вложенной формы. Это немного другой случай, потому что, поскольку поля удаляются, они фактически не удаляются, пока форма не будет отправлена. Я, вероятно, в конечном итоге буду использовать оба, в зависимости от ситуации.
Я основывал свою реализацию на вилке сложных форм Тима Райли на 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;
});
});
К этому времени вы должны иметь базовую динамическую форму! Здесь очень простой javascript, и его можно легко реализовать с помощью других фреймворков. Вы можете легко заменить мой код application.js
, например, на prototype + lowpro. Основная идея состоит в том, что вы не встраиваете гигантские функции javascript в свою разметку, и вам не нужно писать утомительные phone_numbers = ()
функции в ваших моделях. Все просто работает. Ура!
После некоторого дальнейшего тестирования я пришел к выводу, что шаблоны необходимо переместить из полей
. Хранение их там означает, что они будут отправлены обратно на сервер вместе с остальной частью формы, и это только вызовет головную боль позже.
Я добавил это внизу своего макета:
<div id="jstemplates">
<%= yield :jstemplates %>
</div
И изменил 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
Теперь вы можете удалить предложения : reject_if
из своих моделей и перестать беспокоиться о том, что шаблоны будут отправлены обратно.
Основываясь на вашем ответе на мой комментарий, я думаю, что ненавязчивая обработка удаления - хорошее место для начала. Я буду использовать Продукт с каркасами в качестве примера, но код будет общим, поэтому его будет легко использовать в вашем приложении.
Сначала добавьте новую опцию в свой маршрут:
map.resources :products, :member => { :delete => :get }
А теперь добавьте представление удаления в ваши просмотры продуктов:
<% title "Delete Product" %>
<% form_for @product, :html => { :method => :delete } do |f| %>
<h2>Are you sure you want to delete this Product?</h2>
<p>
<%= submit_tag "Delete" %>
or <%= link_to "cancel", products_path %>
</p>
<% end %>
Это представление будет видно только пользователям с отключенным JavaScript.
В контроллере продуктов вам нужно добавить действие удаления.
def delete
Product.find(params[:id])
end
Теперь перейдите в представление индекса и измените ссылку «Удалить» на это:
<td><%= link_to "Delete", delete_product_path(product), :class => 'delete' %></td>
Если вы запустите приложение на этом этапе и просмотрите список продуктов, вы сможете удалить продукт, но мы можем сделать лучше для пользователей с поддержкой JavaScript. Класс, добавленный к ссылке удаления, будет использоваться в нашем JavaScript.
Это будет довольно большой кусок JavaScript, но он ' Важно сосредоточиться на коде, который имеет дело с вызовом ajax - коде в обработчике ajaxSend и обработчике кликов a.delete.
(function() {
var originalRemoveMethod = jQuery.fn.remove;
jQuery.fn.remove = function() {
if(this.hasClass("infomenu") || this.hasClass("pop")) {
$(".selected").removeClass("selected");
}
originalRemoveMethod.apply(this, arguments);
}
})();
function isPost(requestType) {
return requestType.toLowerCase() == 'post';
}
$(document).ajaxSend(function(event, xhr, settings) {
if (isPost(settings.type)) {
settings.data = (settings.data ? settings.data + "&" : "") + "authenticity_token=" + encodeURIComponent( AUTH_TOKEN );
}
xhr.setRequestHeader("Accept", "text/javascript, application/javascript");
});
function closePop(fn) {
var arglength = arguments.length;
if($(".pop").length == 0) { return false; }
$(".pop").slideFadeToggle(function() {
if(arglength) { fn.call(); }
$(this).remove();
});
return true;
}
$('a.delete').live('click', function(event) {
if(event.button != 0) { return true; }
var link = $(this);
link.addClass("selected").parent().append("<div class='pop delpop'><p>Are you sure?</p><p><input type='button' value='Yes' /> or <a href='#' class='close'>Cancel</a></div>");
$(".delpop").slideFadeToggle();
$(".delpop input").click(function() {
$(".pop").slideFadeToggle(function() {
$.post(link.attr('href').substring(0, link.attr('href').indexOf('/delete')), { _method: "delete" },
function(response) {
link.parents("tr").fadeOut(function() {
$(this).remove();
});
});
$(this).remove();
});
});
return false;
});
$(".close").live('click', function() {
return !closePop();
});
$.fn.slideFadeToggle = function(easing, callback) {
return this.animate({opacity: 'toggle', height: 'toggle'}, "fast", easing, callback);
};
Вот CSS, который вам тоже понадобится:
.pop {
background-color:#FFFFFF;
border:1px solid #999999;
cursor:default;
display: none;
position:absolute;
text-align:left;
z-index:500;
padding: 25px 25px 20px;
margin: 0;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
}
a.selected {
background-color:#1F75CC;
color:white;
z-index:100;
}
Нам нужно отправлять токен аутентификации, когда мы выполняем POST, PUT или DELETE. Добавьте эту строку под существующим тегом JS (возможно, в вашем макете):
<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? -%>
Почти готово. Откройте свой контроллер приложений и добавьте эти фильтры:
before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash
И соответствующие методы:
protected
def set_xhr_flash
flash.discard if request.xhr?
end
def correct_safari_and_ie_accept_headers
ajax_request_types = ['text/javascript', 'application/json', 'text/xml']
request.accepts.sort!{ |x, y| ajax_request_types.include?(y.to_s) ? 1 : -1 } if request.xhr?
end
Нам нужно отбросить флэш-сообщения, если это вызов ajax - иначе вы увидите флэш-сообщения из «прошлого» на следующем обычном http запрос. Второй фильтр также необходим для браузеров webkit и IE - я добавляю эти 2 фильтра ко всем своим проектам Rails.
Все, что осталось, это действие destroy:
def destroy
@product.destroy
flash[:notice] = "Successfully destroyed product."
respond_to do |format|
format.html { redirect_to redirect_to products_url }
format.js { render :nothing => true }
end
end
И вот оно. Ненавязчивое удаление с помощью Rails. Кажется, что много работы напечатано, но на самом деле это не так уж и плохо, если вы начнете. Возможно, вас тоже заинтересует этот Railscast .
Кстати. rails немного изменился, поэтому вы больше не можете использовать _delete, теперь используйте _destroy.
def remove_child_link(name, f)
f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child")
end
Также мне было проще просто удалить html, который предназначен для новых записей ... так что я делаю это
$(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('.new_fields').remove();
$(this).parents('.fields').hide();
return false;
});
});