很多程序已经复杂到在一个表单中编辑一个对象已经无法满足需求了。例如,创建 Person
对象时还想让用户在同一个表单中创建多个地址(家庭地址,工作地址,等等)。以后编辑这个 Person
时,还想让用户根据需要添加、删除或修改地址。
9.1 设置模型
Active Record 为此种需求在模型中提供了支持,通过 accepts_nested_attributes_for
方法实现:
class
Person < ActiveRecord::Base
has_many
:addresses
accepts_nested_attributes_for
:addresses
end
class
Address < ActiveRecord::Base
belongs_to
:person
end
|
这段代码会在 Person
对象上创建 addresses_attributes=
方法,用于创建、更新和删除地址(可选操作)。
9.2 嵌套表单
使用下面的表单可以创建 Person
对象及其地址:
<%=
form_for
@person
do
|f|
%>
Addresses:
<
ul
>
<%=
f.fields_for
:addresses
do
|addresses_form|
%>
<
li
>
<%=
addresses_form.label
:kind
%>
<%=
addresses_form.text_field
:kind
%>
<%=
addresses_form.label
:street
%>
<%=
addresses_form.text_field
:street
%>
...
</
li
>
<%
end
%>
</
ul
>
<%
end
%>
|
如果关联支持嵌套属性,fields_for
方法会为关联中的每个元素执行一遍代码块。如果没有地址,就不执行代码块。一般的作法是在控制器中构建一个或多个空的子属性,这样至少会有一组字段显示出来。下面的例子会在新建 Person
对象的表单中显示两组地址字段。
def
new
@person
= Person.
new
2
.times {
@person
.addresses.build}
end
|
fields_for
方法拽入一个表单构造器,参数的名字就是 accepts_nested_attributes_for
方法期望的。例如,如果用户填写了两个地址,提交的参数如下:
{
'person'
=> {
'name'
=>
'John Doe'
,
'addresses_attributes'
=> {
'0'
=> {
'kind'
=>
'Home'
,
'street'
=>
'221b Baker Street'
},
'1'
=> {
'kind'
=>
'Office'
,
'street'
=>
'31 Spooner Street'
}
}
}
}
|
:addresses_attributes
Hash 的键是什么不重要,但至少不能相同。
如果关联的对象已经存在于数据库中,fields_for
方法会自动生成一个隐藏字段,value
属性的值为记录的 id
。把 include_id: false
选项传递给 fields_for
方法可以禁止生成这个隐藏字段。如果自动生成的字段位置不对,导致 HTML 无法通过验证,或者在 ORM 关系中子对象不存在 id
字段,就可以禁止自动生成这个隐藏字段。
9.3 控制器端
像往常一样,参数传递给模型之前,在控制器中要过滤参数:
def
create
@person
= Person.
new
(person_params)
# ...
end
private
def
person_params
params.require(
:person
).permit(
:name
, addresses_attributes: [
:id
,
:kind
,
:street
])
end
|
9.4 删除对象
如果允许用户删除关联的对象,可以把 allow_destroy: true
选项传递给 accepts_nested_attributes_for
方法:
class
Person < ActiveRecord::Base
has_many
:addresses
accepts_nested_attributes_for
:addresses
, allow_destroy:
true
end
|
如果属性组成的 Hash 中包含 _destroy
键,且其值为 1
或 true
,就会删除对象。下面这个表单允许用户删除地址:
<%=
form_for
@person
do
|f|
%>
Addresses:
<
ul
>
<%=
f.fields_for
:addresses
do
|addresses_form|
%>
<
li
>
<%=
addresses_form.check_box :_destroy
%>
<%=
addresses_form.label
:kind
%>
<%=
addresses_form.text_field
:kind
%>
...
</
li
>
<%
end
%>
</
ul
>
<%
end
%>
|
别忘了修改控制器中的参数白名单,允许使用 _destroy
:
def
person_params
params.require(
:person
).
permit(
:name
, addresses_attributes: [
:id
,
:kind
,
:street
, :_destroy])
end
|
9.5 避免创建空记录
如果用户没有填写某些字段,最好将其忽略。此功能可以通过 accepts_nested_attributes_for
方法的 :reject_if
选项实现,其值为 Proc 对象。这个 Proc 对象会在通过表单提交的每一个属性 Hash 上调用。如果返回值为 false
,Active Record 就不会为这个 Hash 构建关联对象。下面的示例代码只有当 kind
属性存在时才尝试构建地址对象:
class
Person < ActiveRecord::Base
has_many
:addresses
accepts_nested_attributes_for
:addresses
, reject_if: lambda {|attributes| attributes[
'kind'
].blank?}
end
|
为了方便,可以把 reject_if
选项的值设为 :all_blank
,此时创建的 Proc 会拒绝为 _destroy
之外其他属性都为空的 Hash 构建对象。
9.6 按需添加字段
我们往往不想事先显示多组字段,而是当用户点击“添加新地址”按钮后再显示。Rails 并没有内建这种功能。生成新的字段时要确保关联数组的键是唯一的,一般可在 JavaScript 中使用当前时间。