使用jQuery Rails的不显眼的dynamic表单字段

我试图克服在Rails的dynamic表单字段的障碍 – 这似乎是框架处理非常优雅的东西。 我也在我的项目中使用jQuery。 我已经安装了jRails,但是我更愿意在不可能的情况下写出AJAX代码。

我的表格相当复杂,两三层嵌套并不罕见。 我遇到的问题是生成正确的表单ID,因为它们依赖于表单构build器上下文。 我需要能够dynamic地添加新字段或删除has_many关系中的现有logging,我完全不知所措。

我到目前为止所看到的每一个例子都是这样或那样的丑陋。 Ryan Bates的教程需要RJS,这会在标记中产生一些相当丑陋的突兀的javascript,而且似乎是在嵌套属性之前编写的。 我已经看到了一个与不显眼的jQuery的例子,但我不明白它在做什么,并没有能够得到它在我的项目中工作。

有人可以提供一个简单的例子来说明如何做到这一点? 这是甚至可以尊重控制器的RESTful约定?


安迪已经发布了删除现有logging的一个很好的例子,任何人都可以提供一个创build具有正确属性的新字段的例子吗? 我一直无法弄清楚如何用嵌套窗体来做到这一点。

由于没有人提供了答案,即使在赏金之后,我终于设法自己做到了这一点。 这不应该是一个史无前例的! 希望这将在Rails 3.0中更容易完成。

Andy的例子是直接删除logging的一种好方法,不用提交表单到服务器。 在这种特殊情况下,我真正想要的是在更新嵌套表单之前dynamic添加/删除字段的方法。 这是一个稍微不同的情况,因为在删除这些字段之前,在提交表单之前它们并没有被删除。 我可能最终会根据情况使用两者。

我已经将我的实现基于github上Tim Riley的复杂forms示例 fork。

首先设置模型,并确保它们支持嵌套的属性:

 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的表单字段创build一个局部视图:

 <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模型创build一组我们可以用javascript复制的模板字段来工作。 我们将在app/helpers/application_helper.rb中为此创build帮助器方法:

 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子句将丢弃它们,只留下用户创build的字段。 更新: 我已经重新考虑这个devise,见下文。

这不是真正的AJAX,因为除了页面加载和表单提交之外没有任何通信进入服务器,但是我真的无法find一个方法来做到这一点。

事实上,这可能比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; }); }); 

到这个时候,你应该有一个准系统的dynamicforms! 这里的JavaScript非常简单,可以很容易地用其他框架来完成。 例如,您可以轻松地将我的application.js代码replace为prototype + lowpro。 基本的想法是,你不是在你的标记中embedded巨大的JavaScript函数,你不必在你的模型中写繁琐的phone_numbers=()函数。 一切正常。 万岁!


经过一些进一步的testing,我已经得出结论,模板需要被移出<form>字段。 将它们保留在那里意味着它们会被送回到服务器,其余的forms,这只是稍后创build头痛。

我已经添加到我的布局的底部:

 <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子句,并停止担心发回的模板。

基于你对我评论的回答,我认为不加引人注意地处理删除是一个很好的开始。 我将以脚手架的产品为例,但是代码是通用的,所以在应用程序中应该很容易使用。

首先为您的路线添加一个新选项:

 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 

现在进入你的索引视图,并将Destroy链接改为:

 <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时,我们需要发送auth令牌。 在你现有的JS标签下添加这行(可能在你的布局中):

 <%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? -%> 

几乎完成。 打开你的应用程序控制器并添加这些filter:

 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调用,我们需要放弃flash消息,否则在下一个常规http请求中会看到来自“过去”的flash消息。 Webkit和IE浏览器也需要第二个filter – 我将这两个filter添加到所有的Rails项目中。

剩下的就是摧毁行动:

 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感兴趣。

顺便一提。 轨道已经改变了一点,所以你不能再使用_delete,现在使用_destroy。

 def remove_child_link(name, f) f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child") end 

另外,我发现删除用于新logging的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; }); }); 

仅供参考,Ryan Bates现在有一个精美的gem: nested_form

我创build了一个不显眼的jQuery插件,用于为Rails 3dynamic添加fields_for对象。它非常易于使用,只需下载js文件即可。 几乎没有configuration。 只要按照惯例,你很好去。

kbparagua/numerous.html

这不是超级灵活的,但它会做的工作。

我成功(而且无痛苦地)使用https://github.com/nathanvda/cocoondynamic生成我的表单。; 优雅地处理关联,文档非常简单。 您也可以将它与simple_form一起使用,这对我特别有用。