使用 fields_for
帮助方法也可创建类似的绑定,但不会生成 <form>
标签。在同一表单中编辑多个模型对象时经常使用 fields_for
方法。例如,有个 Person
模型,和 ContactDetail
模型关联,编写如下的表单可以同时创建两个模型的对象:
<%=
form_for
@person
, url: {action:
"create"
}
do
|person_form|
%>
<%=
person_form.text_field
:name
%>
<%=
fields_for
@person
.contact_detail
do
|contact_details_form|
%>
<%=
contact_details_form.text_field
:phone_number
%>
<%
end
%>
<%
end
%>
|
生成的 HTML 如下:
<
form
accept-charset
=
"UTF-8"
action
=
"/people/create"
class
=
"new_person"
id
=
"new_person"
method
=
"post"
>
<
input
id
=
"person_name"
name
=
"person[name]"
type
=
"text"
/>
<
input
id
=
"contact_detail_phone_number"
name
=
"contact_detail[phone_number]"
type
=
"text"
/>
</
form
>
|
fields_for
方法拽入的对象和 form_for
方法一样,都是表单构造器(其实在代码内部 form_for
会调用 fields_for
方法)。
从前几节可以看出,表单提交的数据可以直接保存在 params
Hash 中,或者嵌套在子 Hash 中。例如,在 Person
模型对应控制器的 create
动作中,params[:person]
一般是一个 Hash,保存创建 Person
实例的所有属性。params
Hash 中也可以保存数组,或由 Hash 组成的数组,等等。
HTML 表单基本上不能处理任何结构化数据,提交的只是由普通的字符串组成的键值对。在程序中使用的数组参数和 Hash 参数是通过 Rails 的参数命名约定生成的。
如果想快速试验本节中的示例,可以在控制台中直接调用 Rack 的参数解析器。例如: T> rubyTIP: Rack::Utils.parse_query "name=fred&phone=0123456789"TIP: # => {"name"=>"fred", "phone"=>"0123456789"}TIP:
7.1 基本结构
数组和 Hash 是两种基本结构。获取 Hash 中值的方法和 params
一样。如果表单中包含以下控件:
<
input
id
=
"person_name"
name
=
"person[name]"
type
=
"text"
value
=
"Henry"
/>
|
得到的 params
值为:
{'person' => {'name' => 'Henry'}}
|
在控制器中可以使用 params[:person][:name]
获取提交的值。
Hash 可以随意嵌套,不限制层级,例如:
<
input
id
=
"person_address_city"
name
=
"person[address][city]"
type
=
"text"
value
=
"New York"
/>
|
得到的 params
值为:
{
'person'
=> {
'address'
=> {
'city'
=>
'New York'
}}}
|
一般情况下 Rails 会忽略重复的参数名。如果参数名中包含空的方括号([]
),Rails 会将其组建成一个数组。如果想让用户输入多个电话号码,在表单中可以这么做:
<
input
name
=
"person[phone_number][]"
type
=
"text"
/>
<
input
name
=
"person[phone_number][]"
type
=
"text"
/>
<
input
name
=
"person[phone_number][]"
type
=
"text"
/>
|
得到的 params[:person][:phone_number]
就是一个数组。
7.2 结合在一起使用
上述命名约定可以结合起来使用,让 params
的某个元素值为数组(如前例),或者由 Hash 组成的数组。例如,使用下面的表单控件可以填写多个地址:
<
input
name
=
"addresses[][line1]"
type
=
"text"
/>
<
input
name
=
"addresses[][line2]"
type
=
"text"
/>
<
input
name
=
"addresses[][city]"
type
=
"text"
/>
|
得到的 params[:addresses]
值是一个由 Hash 组成的数组,Hash 中的键包括 line1
、line2
和city
。如果 Rails 发现输入框的 name
属性值已经存在于当前 Hash 中,就会新建一个 Hash。
不过有个限制,虽然 Hash 可以嵌套任意层级,但数组只能嵌套一层。如果需要嵌套多层数组,可以使用 Hash 实现。例如,如果想创建一个包含模型对象的数组,可以创建一个 Hash,以模型对象的 ID、数组索引或其他参数为键。
数组类型参数不能很好的在 check_box
帮助方法中使用。根据 HTML 规范,未选中的复选框不应该提交值。但是不管是否选中都提交值往往更便于处理。为此 check_box
方法额外创建了一个同名的隐藏 input
元素。如果没有选中复选框,只会提交隐藏 input
元素的值,如果选中则同时提交两个值,但复选框的值优先级更高。处理数组参数时重复提交相同的参数会让 Rails 迷惑,因为对 Rails 来说,见到重复的 input
值,就会创建一个新数组元素。所以更推荐使用 check_box_tag
方法,或者用 Hash 代替数组。
7.3 使用表单帮助方法
前面几节并没有使用 Rails 提供的表单帮助方法。你可以自己创建 input
元素的 name
属性,然后直接将其传递给 text_field_tag
等帮助方法。但是 Rails 提供了更高级的支持。本节介绍 form_for
和 fields_for
方法的 name
参数以及 :index
选项。
你可能会想编写一个表单,其中有很多字段,用于编辑某人的所有地址。例如:
<%=
form_for
@person
do
|person_form|
%>
<%=
person_form.text_field
:name
%>
<%
@person
.addresses.
each
do
|address|
%>
<%=
person_form.fields_for address, index: address.id
do
|address_form|
%>
<%=
address_form.text_field
:city
%>
<%
end
%>
<%
end
%>
<%
end
%>
|
假设这个人有两个地址,ID 分别为 23 和 45。那么上述代码生成的 HTML 如下:
<
form
accept-charset
=
"UTF-8"
action
=
"/people/1"
class
=
"edit_person"
id
=
"edit_person_1"
method
=
"post"
>
<
input
id
=
"person_name"
name
=
"person[name]"
type
=
"text"
/>
<
input
id
=
"person_address_23_city"
name
=
"person[address][23][city]"
type
=
"text"
/>
<
input
id
=
"person_address_45_city"
name
=
"person[address][45][city]"
type
=
"text"
/>
</
form
>
|
得到的 params
Hash 如下:
{
'person'
=> {
'name'
=>
'Bob'
,
'address'
=> {
'23'
=> {
'city'
=>
'Paris'
},
'45'
=> {
'city'
=>
'London'
}}}}
|
Rails 之所以知道这些输入框中的值是 person
Hash 的一部分,是因为我们在第一个表单构造器上调用了 fields_for
方法。指定 :index
选项的目的是告诉 Rails,其中的输入框 name
属性值不是 person[address][city]
,而要在 address
和 city
索引之间插入 :index
选项对应的值(放入方括号中)。这么做很有用,因为便于分辨要修改的 Address
记录是哪个。:index
选项的值可以是具有其他意义的数字、字符串,甚至是 nil
(此时会新建一个数组参数)。
如果想创建更复杂的嵌套,可以指定 name
属性的第一部分(前例中的 person[address]
):
<%=
fields_for
'person[address][primary]'
, address, index: address
do
|address_form|
%>
<%=
address_form.text_field
:city
%>
<%
end
%>
|
生成的 HTML 如下:
<
input
id
=
"person_address_primary_1_city"
name
=
"person[address][primary][1][city]"
type
=
"text"
value
=
"bologna"
/>
|
一般来说,最终得到的 name
属性值是 fields_for
或 form_for
方法的第一个参数加 :index
选项的值再加属性名。:index
选项也可直接传给 text_field
等帮助方法,但在表单构造器中指定可以避免代码重复。
为了简化句法,还可以不使用 :index
选项,直接在第一个参数后面加上 []
。这么做和指定 index: address
选项的作用一样,因此下面这段代码
<%=
fields_for
'person[address][primary][]'
, address
do
|address_form|
%>
<%=
address_form.text_field
:city
%>
<%
end
%>
|
生成的 HTML 和前面一样。