Ruby on Rails 嵌套属性
介绍
嵌套属性是一种功能,可让您通过其关联的父级保存记录的属性。在此示例中,我们将考虑以下场景:
我们正在创建一个包含大量产品的在线商店。每个产品可以有零个或多个变体。变体就是字面意思;它们代表同一产品的不同变体,例如颜色不同。两者都有名称和价格。每个产品还将与一个图像记录相关联,其中包含一个网址、alt 和一个标题。
在本教程的后面,我们将改进这些模型:
class Product < ActiveRecord::Base
has_many :variants
has_one :image
# Attributes: name:string, price:float
end
class Variant < ActiveRecord::Base
belongs_to :product
# Attributes: name:string, price:float
end
class Image < ActiveRecord::Base
belongs_to :product
# Attributes: url:string, alt:string caption:string
end
一对一关联
嵌套属性最简单的例子是一对一关联。要为产品模型添加嵌套属性支持,只需添加以下行:
class Product < ActiveRecord::Base
has_many :variants
accepts_nested_attributes_for :image
end
这到底是做什么的?它将把已保存的属性从产品模型代理到图像模型。在产品表单中,您需要添加用于图像关联的其他字段。您可以使用 fields_for助手来完成此操作。
= form_for @product do |f|
// Product attributes
.form-group
= f.label :name
= f.text_field :name
.form-group
= f.label :price
= f.text_field :price
// Image attributes
= f.fields_for :image do |f|
= f.label :url
= f.text_field :url
= f.label :alt
= f.text_field :alt
= f.label :caption
= f.text_field :caption
= f.submit
现在,剩下的唯一部分就是修改控制器以接受这些新属性。嵌套属性背后的整个想法是,您不必在控制器中添加额外的代码来处理此输入和关联,但您确实需要允许这些属性到达模型,而强参数默认会阻止这种情况。因此,您需要将以下内容添加到ProductsController中的product_params方法中。
def product_params
params.require(:product).permit(
:name, :price,
image_attributes: [ :id, :url, :alt, :caption ]
)
end
瞧!现在您可以从同一表单内联编辑产品模型的图像关联。现在让我们看看如何通过多对多关系构建相同的行为。
多对多关联
产品变体非常简单(只有两个字段),因此没有必要创建单独的页面来编辑它们。相反,我们希望从同一个产品表单中内联编辑它们以及产品属性。由于每个产品可以有多种变体,这意味着我们必须处理多个项目。我们还需要添加新变体并删除旧变体。让我们逐一解决这些问题。
显示多个关联
fields_for方法为每个关联记录生成一个块,因此我们不需要更改任何内容 - 但因为我们需要重复使用此表单(为了通过 JavaScript 自动添加新字段),所以我们需要将其移动到单独的文件中。我们将创建一个名为_variant_fields.slim的新部分,其中仅包含变体字段,如下所示:
= f.label :name
= f.text_field :name
= f.label :price
= f.text_field :price
回到产品表单,为了呈现字段,我们只需利用fields_for为每个关联产生一个块的事实,并将表单帮助对象传递给部分。
= f.fields_for :variants do |f|
= render 'variant_fields', f: f
添加新关联
为了添加新的关联,我们需要创建一些添加新字段的 JavaScript。我喜欢创建一个链接,当单击该链接时,它将添加一个新的字段元组。如下所示:
= link_to_add_fields 'Add Product Variant', f, :variants
这是我编写的一个有用的辅助方法,它将创建一个带有data-form-prepend属性的链接,其中包含_variant_fields.slim部分的全部内容。这里的想法是,当您单击它时,您将使用一些简单的可重复使用的 JavaScript 将这些字段附加到表单的末尾。
实际的帮助程序看起来相当复杂和混乱,但请耐心等待——我保证它和大多数代码一样简单。它只处理参数,关键逻辑位于最后七行。您可以将此代码放在 application_helper.rb中。
def link_to_add_fields(name = nil, f = nil, association = nil, options = nil, html_options = nil, &block)
# If a block is provided there is no name attribute and the arguments are
# shifted with one position to the left. This re-assigns those values.
f, association, options, html_options = name, f, association, options if block_given?
options = {} if options.nil?
html_options = {} if html_options.nil?
if options.include? :locals
locals = options[:locals]
else
locals = { }
end
if options.include? :partial
partial = options[:partial]
else
partial = association.to_s.singularize + '_fields'
end
# Render the form fields from a file with the association name provided
new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, child_index: 'new_record') do |builder|
render(partial, locals.merge!( f: builder))
end
# The rendered fields are sent with the link within the data-form-prepend attr
html_options['data-form-prepend'] = raw CGI::escapeHTML( fields )
html_options['href'] = '#'
content_tag(:a, name, html_options, &block)
end
在 JavaScript 方面,我使用 jQuery 查找每个将 name 属性设置为new_record的元素,并将其替换为时间戳。这解决了添加多条新记录时的问题;两条记录将具有相同的 id ( new_record )。
$("[data-form-prepend]").click(function(e) {
var obj = $($(this).attr("data-form-prepend"));
obj.find("input, select, textarea").each(function() {
$(this).attr("name", function() {
return $(this)
.attr("name")
.replace("new_record", new Date().getTime());
});
});
obj.insertBefore(this);
return false;
});
删除关联
幸运的是,accepts_nested_attributes_for有一些删除关联的巧妙功能。如果我们将allow_destroy: true参数传递给accepts_nested_attributes_for ,它将销毁包含_destroy键的属性中的任何成员。
accepts_nested_attributes_for :variants, allow_destroy: true
在视图中,这可以通过一个简单的复选框来实现。所以我在_variant_fields.slim中添加了一个复选框:
= f.check_box :_destroy
= f.label :delete
对模型和控制器的修改
再次,与以前一样, ProductsController中添加的只是product_params方法,现在还应该包括variants_attributes。
def product_params
params.require(:product).permit(
:name, :price,
image_attributes: [ :id, :url, :alt, :caption ],
variants_attributes: [ :id, :name, :price, :_destroy ]
)
end
产品模型只需添加以下内容即可为变体关联启用嵌套属性:
accepts_nested_attributes_for :variants, reject_if: :all_blank, allow_destroy: true
注意reject_if :all_blank选项。这意味着任何属性全为空白(不包括_destroy的值)的记录都将被拒绝。reject_if还支持传递一个Proc,可用于一些额外的验证,它还会检查是否拒绝/包含关联。
还有两个有用的选项。第一个是limit选项,它指定将处理的最大记录数。第二个选项是update_only,它适用于一对一关联,并且具有相当有趣的行为。如果将其设置为 true,它将仅更新关联记录的属性。如果在更改时设置为 false,它不会触及旧记录,但会使用新属性创建一个新记录。默认情况下,它是 false,并且会创建一个新记录,除非记录包含id属性,这正是我们在product_params中包含id属性的原因,用于一对一图像关联。另一种解决方案是定义嵌套属性,如下所示:
accepts_nested_attributes_for :image, update_only: true
您可以在 Ruby on Rails 文档中阅读有关嵌套属性的更多信息,其中每个配置选项都有演示和详尽的记录。
关于作者
Itay Grudev 是一名学生,目前正在英国阿伯丁大学攻读计算机科学和物理学学位。</p
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~