Rails 中使用 jQuery 的不显眼的动态表单字段
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1704142/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Unobtrusive dynamic form fields in Rails with jQuery
提问by Adam Lassek
I'm attempting to get over the hurdle of dynamic form fields in Rails -- this appears to be something the framework doesn't handle very gracefully. I'm also using jQuery in my project. I have jRails installed, but I'd much rather write the AJAX code unobtrusively where possible.
我正在尝试克服 Rails 中动态表单字段的障碍——这似乎是框架不能很好地处理的事情。我也在我的项目中使用 jQuery。我已经安装了 jRails,但我更愿意尽可能不引人注意地编写 AJAX 代码。
My forms are fairly complex, two or three levels of nesting are not unusual. The problem I'm having is generating the correct form ids, since they are so dependant on the form builder context. I need to be able to dynamically add new fields or delete existing records in a has_many
relationship, and I am completely at a loss.
我的表格相当复杂,两三层嵌套并不罕见。我遇到的问题是生成正确的表单 ID,因为它们非常依赖于表单构建器上下文。我需要能够动态添加新字段或删除has_many
关系中的现有记录,我完全不知所措。
Every example I've seen so far has been ugly in one way or another. Ryan Bates' tutorialrequires RJS, which results in some pretty ugly obtrusive javascript in the markup, and seems to have been written before nested attributes. I've seen a forkof that example with unobtrusive jQuery, but I just don't understand what it's doing, and haven't been able to get it working in my project.
到目前为止,我所看到的每个例子都在某种程度上是丑陋的。Ryan Bates 的教程需要 RJS,这会导致标记中出现一些非常难看的 javascript,并且似乎是在嵌套属性之前编写的。我已经用不显眼的 jQuery看到了那个例子的一个分支,但我只是不明白它在做什么,并且无法让它在我的项目中工作。
Can somebody provide a simple example of how this is done? Is this even possible while respecting the RESTful convention of the controllers?
有人可以提供一个简单的例子来说明这是如何完成的吗?在尊重控制器的 RESTful 约定的同时,这甚至可能吗?
Andy has posted an excellent example of deleting an existing record, can anybody provide an example of creating new fields with the correct attributes? I haven't been able to figure out how to do this with nested forms.
Andy 发布了一个删除现有记录的极好示例,有人可以提供一个创建具有正确属性的新字段的示例吗?我一直无法弄清楚如何使用嵌套表单来做到这一点。
回答by Adam Lassek
Since nobody has offered an answer to this, even after a bounty, I've finally managed to get this working myself. This wasn't supposed to be a stumper! Hopefully this will be easier to do in Rails 3.0.
由于没有人对此提供答案,即使在赏金之后,我也终于设法自己解决了这个问题。这不应该是绊脚石!希望这在 Rails 3.0 中更容易做到。
Andy's example is a good way of deleting records directly, without submitting a form to the server. In this particular case, what I'm really looking for is a way to dynamically add/remove fields before doing an update to a nested form. This is a slightly different case, because as the fields are removed, they aren't actually deleted until the form is submitted. I will probably end up using both depending on the situation.
Andy的例子是直接删除记录的好方法,不用向服务器提交表单。在这种特殊情况下,我真正要寻找的是一种在更新嵌套表单之前动态添加/删除字段的方法。这是一个稍微不同的情况,因为随着字段被删除,在提交表单之前它们实际上并没有被删除。根据情况,我可能最终会同时使用两者。
I've based my implementation on Tim Riley's complex-forms-examplesfork on github.
我的实现基于Tim Riley在 github 上的 complex-forms-examplesfork。
First set up the models, and make sure they support nested attributes:
首先设置模型,并确保它们支持嵌套属性:
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
Create a partial view for the PhoneNumber's form fields:
为 PhoneNumber 的表单域创建一个局部视图:
<div class="fields">
<%= f.text_field :description %>
<%= f.text_field :number %>
</div>
Next write a basic edit view for the Person model:
接下来为 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 -%>
This will work by creating a set of template fields for the PhoneNumber model that we can duplicate with javascript. We'll create helper methods in app/helpers/application_helper.rb
for this:
这将通过为 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
Now add these helper methods to the edit partial:
现在将这些辅助方法添加到编辑部分:
<% 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 -%>
You now have the js templating done. It will submit a blank template for each association, but the :reject_if
clause in the model will discard them, leaving only the user-created fields. Update:I've rethought this design, see below.
您现在已经完成了 js 模板。它将为每个关联提交一个空白模板,但:reject_if
模型中的子句将丢弃它们,只留下用户创建的字段。更新:我重新考虑了这个设计,见下文。
This isn't truly AJAX, since there isn't any communication going on to the server beyond the page load and form submit, but I honestly could not find a way to do it after the fact.
这不是真正的 AJAX,因为除了页面加载和表单提交之外,没有任何与服务器进行的通信,但老实说,事后我找不到办法做到这一点。
In fact this may provide a better user experience than AJAX, since you don't have to wait for a server response for each additional field until you're done.
事实上,这可能会提供比 AJAX 更好的用户体验,因为在完成之前您不必等待每个附加字段的服务器响应。
Finally we need to wire this up with javascript. Add the following to your `public/javascripts/application.js' file:
最后,我们需要使用 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;
});
});
By this time you should have a barebones dynamic form! The javascript here is really simple, and could easily be done with other frameworks. You could easily replace my application.js
code with prototype + lowpro for instance. The basic idea is that you're not embedding gigantic javascript functions into your markup, and you don't have to write tedious phone_numbers=()
functions in your models. Everything just works. Hooray!
这时候你应该有一个准系统的动态表单!这里的javascript真的很简单,可以很容易地用其他框架来完成。例如,您可以轻松地application.js
用原型 + lowpro替换我的代码。基本思想是您没有将巨大的 javascript 函数嵌入到您的标记中,并且您不必phone_numbers=()
在您的模型中编写乏味的函数。一切正常。万岁!
After some further testing, I've concluded that the templates need to be moved out of the <form>
fields. Keeping them there means they get sent back to the server with the rest of the form, and that just creates headaches later.
经过一些进一步的测试,我得出结论,模板需要移出<form>
字段。将它们保留在那里意味着它们会与表单的其余部分一起发送回服务器,这只会在以后造成麻烦。
I've added this to the bottom of my layout:
我已将此添加到布局的底部:
<div id="jstemplates">
<%= yield :jstemplates %>
</div
And modified the new_child_fields_template
helper:
并修改了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
Now you can remove the :reject_if
clauses from your models and stop worrying about the templates being sent back.
现在,您可以:reject_if
从模型中删除子句,而不必担心模板被发回。
回答by Andy Gaskell
Based on your answer to my comment I think handling deletion unobtrusively is a good place to start. I'll use Product with scaffolding as an example, but the code will be generic so it should be easy to use in your application.
根据您对我的评论的回答,我认为不引人注目地处理删除是一个很好的起点。我将使用带有脚手架的 Product 作为示例,但代码将是通用的,因此应该很容易在您的应用程序中使用。
First add a new option to your route:
首先向您的路线添加一个新选项:
map.resources :products, :member => { :delete => :get }
And now add a delete view to your Product views:
现在向您的产品视图添加删除视图:
<% 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 %>
This view will only be seen by users with JavaScript disabled.
只有禁用 JavaScript 的用户才能看到此视图。
In the Products controller you'll need to add the delete action.
在 Products 控制器中,您需要添加删除操作。
def delete
Product.find(params[:id])
end
Now go to your index view and change the Destroy link to this:
现在转到您的索引视图并将销毁链接更改为:
<td><%= link_to "Delete", delete_product_path(product), :class => 'delete' %></td>
If you run the app at this point and view the list of products you'll be able to delete a product, but we can do better for JavaScript enabled users. The class added to the delete link will be used in our JavaScript.
如果您此时运行应用程序并查看产品列表,您将能够删除产品,但我们可以为启用 JavaScript 的用户做得更好。添加到删除链接的类将在我们的 JavaScript 中使用。
This will be a rather large chunk of JavaScript, but it's important to focus on the code that deals with making the ajax call - the code in the ajaxSend handler and the 'a.delete' click handler.
这将是一个相当大的 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);
};
Here's the CSS you'll need too:
这也是你需要的 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;
}
We need to send along the auth token when we make POST, PUT or DELETE. Add this line under your existing JS tag (probably in your layout):
当我们进行 POST、PUT 或 DELETE 时,我们需要发送 auth 令牌。在您现有的 JS 标签下添加这一行(可能在您的布局中):
<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? -%>
Almost done. Open up your Application controller and add these filters:
快完成了。打开您的应用程序控制器并添加这些过滤器:
before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash
And the corresponding methods:
以及相应的方法:
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
We need to discard flash messages if it's an ajax call - otherwise you'll see flash messages from the "past" on your next regular http request. The second filter is also required for webkit and IE browsers - I add these 2 filters to all of my Rails projects.
如果是 ajax 调用,我们需要丢弃 flash 消息 - 否则您将在下一个常规 http 请求中看到来自“过去”的 flash 消息。webkit 和 IE 浏览器也需要第二个过滤器 - 我将这两个过滤器添加到我所有的 Rails 项目中。
All that's left is the destroy action:
剩下的就是销毁操作:
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
And there you have it. Unobtrusive deleting with Rails. It seems like a lot of work all typed out, but it's really not that bad once you get going. You might be interested in this Railscasttoo.
你有它。使用 Rails 进行不显眼的删除。看起来很多工作都打出来了,但一旦你开始,它真的没那么糟糕。您可能也对这个Railscast感兴趣。
回答by David Henner
By the way. rails has changed a bit so you can not longer use _delete, now use _destroy.
顺便一提。rails 发生了一些变化,因此您不能再使用 _delete,现在使用 _destroy。
def remove_child_link(name, f)
f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child")
end
Also I found it easier to just remove html that is for new records... so i do this
另外我发现删除新记录的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;
});
});
回答by Garrett Lancaster
FYI, Ryan Bates now a has a gem that works beautifully: nested_form
仅供参考,Ryan Bates 现在有一个非常漂亮的宝石:nested_form
回答by Karl Paragua
I created an unobtrusive jQuery plugin for dynamically adding fields_for objects for Rails 3. It is very easy to use, just download the js file. Almost no configuration. Just follow the conventions and you're good to go.
我创建了一个不显眼的 jQuery 插件,用于为 Rails 3 动态添加 fields_for 对象。它非常易于使用,只需下载 js 文件。几乎没有配置。只需遵循惯例,您就可以开始了。
https://github.com/kbparagua/numerous.js
https://github.com/kbparagua/numerous.js
It is not super flexible but it will do the job.
它不是超级灵活,但可以胜任。
回答by roobeedeedada
I've successfully (and rather painlessly) used https://github.com/nathanvda/cocoonto dynamically generate my forms. Handles associations elegantly and the documentation is very straightforward. You can use it together with simple_form, too, which was particularly useful for me.
我已经成功地(而且相当轻松地)使用了https://github.com/nathanvda/cocoon来动态生成我的表单。优雅地处理关联,文档非常简单。您也可以将它与 simple_form 一起使用,这对我特别有用。