form 与 表单对象。
前方高能:
- 本节内容比较多。比较烧脑。大家要务必作作业。不动手,学不会。
关于提到的各种helper, 多看官方文档:
也可以看API中的文档(写的也特别细) http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for
目录:
- 十年前的 form 表单,在后台是如何处理的。
- 演进过程。
- 表单对象的优缺点,好处
复习: 浏览器,传数据给服务器,能传几种数据类型?
三种.
1.字符串.例如: /some_url?a=123
a = '123'
2.数组. 例如: /some_url?a[]=1&a[]=2&a[]=3
可以知道, a = ['1', '2', '3']
对于Rails的Controller, 可以处理为:
Started GET "/books?a[]=1&a[]=2&a[]=3" for 127.0.0.1 at 2016-10-06 15:48:10 +0800
Processing by BooksController#index as HTML
Parameters: {"a"=>["1", "2", "3"]}
可以看到 Rails中, 也是一样处理的.
3.Hash. 例如:
/some_url?student[name]=jim&student[age]=20
student = { name: "jim", age: "20" }
对于Rails的日志:
Started GET "/books?student[name]=jim&student[age]=20" for 127.0.0.1 at 2016-10-06 15:48:52 +0800
Processing by BooksController#index as HTML
Parameters: {"student"=>{"name"=>"jim", "age"=>"20"}}
以上形式, 对于所有类型的http request都使用.
如果我要发起一个POST 请求
POST /ooxx.html (form)
就要参数放到form 表单中。
<form action='ooxx.html' method=POST>
<input name='a' value='1'/>
<input name='b' value='1'/>
....
</form>
如果我有一个对象,叫 article(中文名: 帖子), 它有两个属性: title
, content
.
那么,我们在Rails中,约定, 对应的html表单,就应该写成::
<form action='/articles' method='post' >
<input type='text' name='article[title]' value= '我是标题' />
<input type='text' name='article[content]' value= '我是正文' />
</form>
这样, 我们传到服务器端的参数, 就是:
{
article: {
title: "我是标题",
content: "我是正文"
}
}
于是,就可以很方便的让我们后端的代码来处理。后端代码可以非常的简单的 把上面的hash转换成对应的对象。
rails是可以自动分辨, 某个参数的值,是: 字符串,数组, 还是hash
<form action='..' method='..'>
<input type='text' name='article[title]' value='三体'/> params[:article][:title] => '三体'
<input type='text' name='colors[]' value='green'/>
<input type='text' name='colors[]' value='red'/>
<input type='text' name='colors[]' value='yellow'/>
params[:colors] => ['green', 'red', 'yellow']
<input type='text' name='article[readers][]' value='jim'/>
<input type='text' name='article[readers][]' value='lilei'/>
params[:article][:readers] => ["jim", "lilei"]
</form>
上面的form表单, 传递的参数, 实际上是一个大hash, 形如:
{
article: {
title: '三体',
colors: ['green', 'red', 'yellow'],
readers : [
"jim",
"lilei"
],
}
}
(也可以在hash里面,放数组,再放hash, 再数组。 总之,可以各种混用。 但是: 实际工作中,不要这样用。会被同事骂。 也会把自己绕蒙)
于是,我们从十年前,比较原始的web开发时代开始说起.
对于下面的这个表单:
<form action='/articles' method='post' >
<input type='text' name='article[title]' value= '我是标题' />
<input type='text' name='article[content]' value= '我是正文' />
</form>
它传给后端的值也就是:
{
article: {
title: "我是标题",
content: "我是正文"
}
}
10 年前的java代码,是如何处理的呢?
// request 是 一个内置的对象
// step1. 获取浏览器传过来的所有参数
String title = request.getParameter('article["title"]');
String content = request.getParameter('article["content"]');
// step2. 初始化一个 model. 并且 设置它的各种值
Article article = new Article();
article.setTitle(title);
article.setContent(content);
// step3. 保存
article.save();
问题就出现了: 一个form 表单有很多个参数怎么办?
对象的属性越多,传递过来的参数就越多,上面的赋值语句就越多。
我曾经见过 有20行语句, 都是: request.getParameter('...')
所以,解决方法: 使用表单对象 form object
。来对这么多个参数进行封装.
最开始的表单对象: 需要手动创建一个object:
注释掉。 /form/ 用来出现再 form/ controller 中, 代表表单对象。
这个model 用来操作数据库的。
(java struts框架的里面的 臭名昭著的东东, 2005年)
class Article {
private String title;
private String content;
//getter, setter...
}
所以这就尴尬了: 表单对象,跟数据库的ORM对象,一模一样。导致了同样的代码出现在了 两个文件当中。
知识点: 使用ORM后,做数据库操作时, 有三个层次要参与:
- form object
- ORM model
- 数据库table
以上面的 article
为例子。 有两个属性: title
, content
.
纯HTML | form object | ORM model | DB TABLE |
---|---|---|---|
1. 纯html
rails是可以自动分辨, 某个参数的值,是: 字符串,数组, 还是hash
<form action='..' method='..'>
<input type='text' name='article[title]'/> params[:article][:title]
<input type='content' name='article[content]'/> params[:article][:content]
</form>
2. form object:
在rails中是隐形的。你看不到它的声明。因为:它是在运行时,被rails中的某些方法动态创建的。
p.s. 动态创建方法的例子(javascript)
my_string = 'function hi(){ console.info("hi") }'
"function hi(){ console.info("hi") }"
eval(my_string)
undefined
hi() # =
(在其他语言和框架中,这个对象,都是 显式 声明的(你的手写出来), 在struts中就要这样)
rails中, 动态创建的form object, 理论上是这样: (form 中包含什么参数, 或者说数据库中有多少列, 就有多少attribute)
class Article
attr_accessor :title
attr_accessor :content
end
通过 form_for 来使用表单对象.
所以,rails中, form 就要这样写:
<% form_for @aritcle do |f| %>
<%= f.text_field :title %>
<%= f.text_field :content %>
<% end %>
上面的重点,在于: do ... end
. 它说明了rails 是如何调用 上面的隐形的表单对象的。
这一句:
<%= f.text_field :title %>
就是调用了 上面的 Article
的 attr_accessor :title
.
如果说article
是个表单对象的话,
- 编辑 article的时候, 要显示原来的值, 就是:
article.get_title
- 保存 article的时候, 要保存传过来的值,就是:
article.title=
在上面的 do |f| ... end
中, 这个f
就是表单对象.
f.text_field :title
不但会生成一个<input type='text' />
标签,而且还会通过调用表单对象的 .get_title
方法,
来为这个<input/>
标签设置初始值.
3. ORM model.
app/models 目录下的article.rb:
class Article < ActiveRecord::Base
# rails会自动生成: title, content 的 accessor
end
(也可以认为, 在Rails中, ORM model 跟 form object model 是一个文件。
4. DB table:
articles表: |
---|
第一个列:title |
第二个列:content |
表单对象与持久层,在Rails中是一个.
- 表单对象(处理表单代码时, 把参数保存到 对象中)
- 持久层(把对象中的数据保存到DB)
在rails中这两个是一样的东东。
使用Hash为Model赋值.
Rails形式: 它的宗旨就是 方便程序员, 对人友好:
回到刚才的java代码: 可以看到,给对象的赋值是一个一个来的:
// 这个就是form 提交过来的 参数: { article: { title: "我是标题", content: "我是正文" } }
String title = request.getParameter('article["title"]');
String content = request.getParameter('article["content"]');
Article article = new Article();
article.setTitle(title);
article.setContent(content);
article.save();
演变1: (一个属性一个属性的赋值)
该写法常见于: 从java转行过来的rails程序员。
article = Article.new({
:title => params['article']['title'],
:content => params['article']['content']
})
article.save # 在这里执行 insert into articles values (...)
演变2:(直接给构造函数一个hash , 再save)
article = Article.new params['article']
article.save
演变3:(把 new ... save
的步骤, 省略成: create
)
Article.create params['article']
引申: java 与 ruby的不同( 语言能力上的,特别是元编程的不同)
对于持久层的model 声明:
我们假设articles
表, 有两个列: title
, content
:
class Article < ActiveRecord::Base
end
它就会自动出现下面的方法:
article = Article.create ...
article.title # => 获得title
article.content # => 获得content
java代码则是任何方法都要手写:
public Article extends ...{
private String title;
public String getTitle() { ...} // 需要手动定义
public void setTitle() { ... }
}
TODO 送给好奇宝宝
元编程动态声明方法的例子:
可以看出ruby元编程极其简单.
class Post
end
post = Post.new
post.instance_eval("
def say_hi
puts 'hihihi'
end
")
post.say_hi
# => 'hihihi'
form_for
下面是一个最常见的Rails表单:
<%= form_for @user do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
form_for
是个方法。
要三个参数:
form_for(record, options = {}, &block)
对于下面的代码:
<%= form_for @user do |f| %>
record
:是 @user
,
options
, 就是 {}
, 所以被省略掉了。
最后一个参数,是 block
.
- 为什么,第二个参数可以被省略掉?
因为, 在ruby当中, 规定: 如果一个函数的参数中,有block, 那么这个block, 必须是最后一个参数。
所以,ruby ,要解析一个函数的时候,一开始,就能知道, do ... end 之间,这是一个 block, 所以,它就是最后的参数。
而: form_for @user, 表示, form_for的第一个参数,就是 @user.
所以,根据form_for的定义:
- 第一个参数,有了。
- 第二个参数,在定义中,就是: 可选的。 ( options = {} )
- 第三个参数:( &block ) 也有了。
所以这个函数就完整了,可以正常运行了。
下面的 f.label
, f.text_field
, f.submit
, 中的 f
:
<%= form_for @user do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
这个f
, 就是block中的参数,也可以直接把它看成: form object
. (表单对象)
预习 rails中的简写。
- 为什么有时候是 :var, 有时候是 @var 由 api 决定的。 以及 rails 的省略手法决定的。
例如: 我们要访问某个 controller的某个action:
打开: http://localhost:3000/users?name=dashi
如果我要获取到这个url 的参数:
在rails中,有三种方式:
params[:name]
params['name']
params["name"]
所以,form_for 的第一个参数,也可以写成下面的三种形式。
<%= form_for @name %> 写成这样。 下面两种,不推荐。除非你特别有把握。
<%= form_for :name %>
<%= form_for 'name' %>
"name" | 'name' | :name
string | string | symbol
symbol: 不变的字符串。 ruby 的概念。
<%= form_for @post do |f| %>
...
<% end %>
就等同于:
<%= form_for @post,
as: :post,
url: post_path(@post),
method: :patch,
html: { class: "edit_post", id: "edit_post_45" }
do |f| %>
# 其他代码
<% end %>
问题来了: 上面的代码中, form_for 有多少个参数呢?
如果你看了API, 就知道了,它有3个参数。
但是,为什么, as, url, method, 都不是参数呢? 因为,他们是一个hash, 在ruby中, hash最外层的大括号很多时候可以省略。
哪些时候可以省略呢?
该hash作为参数时,如果它不是最后一个非block参数。它就可以省略大括号。
form_for @user, { url: '', method: '' } do |f| ...
end
那么问题来了:
为什么要用rails的自定义html 标签呢? , 例如:
form_for,
form_tag,
text_field,
select_tag
而不是:
<form> , <input> ...
答: 为了更加简单
例如, 比较下面两种写法:
1.form的rails写法:
<%= form_for @post, html: { class: "edit_post", id: "edit_post_45" } do |f| %>
<%= f.text :title, '我是标题' %>
<% end %>
2.form的HTML写法:
<form action='/posts' method='post' class='edit_post' id='edit_post_45' >
<input type='text' name='post[title]' value= '我是标题' />
</form>
我们发现两者差别不大.
但是, 在做 编辑某个记录的时候, 我需要:
- 生成一个form object
- 生成的html 标签,带有默认值.
(下面就是php风格:)
<form action=.. >
<input type='text' name='post[title]' value=<%= @post.title %> />
<input type='text' name='post[content]' value=<%= @post.content %> />
</form>
(下面就是ruby风格)
<%= form_for @post do |f| %>
<%= f.text_field :title %>
<% end %>
所以,php的代码,看起来就是场 噩梦。
例如:某个下拉框,有100个选项. 就要搞100次循环。然后,还要判断默认值。 那个时候,就会觉得代码特别的臃肿。
TODO: 把臃肿的代码,COPY到这里。或者做个截图.
form_for 很智能的地方。
过程: rails发现了 form_for
的唯一参数: @post, 它就会开始按照"约定" 来猜测
各种参数: form_object
, method
, action
...
所以,对于 新建操作的form:
<form action='/articles' method = 'post' >
</form>
和对于 编辑操作的form:
<form action='/articles/3/edit' method = 'post' >
<input type='hidden' name='_method' value='put'/>
</form>
变成ruby代码的话就是:
<% if @article.id.present? %>
<form action='/articles/3/edit' method = 'put' >
<% else %>
<form action='/articles' method = 'post' >
<% end %>
于是,我们就可以用:
<%= form_for @post %>
这段代码, 来表示上面提到的两种场合(@post 是已经存在的记录,还是没有存在的记录).
form 中的authentity_token
每一个 <form>
中,都有一个authentity_token
. 防止注入攻击(XSS).
比如说,我在购物时,需要提交表单, 付费 . 这个表单当中,很可能就是:
<form ... >
<input name='price' value='1000'/>
</form>
这个form 是非常好伪造的. 所以,需要用authentity_token 来识别.
<form ... >
<input type='hidden' name='authentity_token' value='a1b2c3d4.....' />
<input name='price' value='1000'/>
</form>
authentity_token
是由服务器端(rails)生成的. 它针对不同的browser/session 生成不同的token .
于是我们就可以在服务器端做验证了.
class PostsController ...
protec_from_forgery # 这行代码的作用: 对每个form 做验证,看里面的token 是否跟服务器端匹配.
end
<% if @post.id.present? %>
<form action='/posts/3/edit' method = 'put' >
<% else %>
<form action='/posts' method = 'post' >
<% end %>
<input type='hidden' name='token' value='<%= generate_token %>' />
所以使用form tag
, 就再也不用人肉写上面那行的代码了。
<input type='hidden' name='token' value='<%= generate_token %>' />
如果使用了form tag, 就可以自动生成 csrf token了.:
<%= form_for @post do |f| %>
使用form object 生成输入项的默认值.
text_field, select 如何保证在编辑某个属性的时候, 它能选中了某个默认值?
<select>
<option name=...> value</option>
<option name=... <%=if @post.title == 'xx' %> selected <% end %>> value</option>
<option name=... selected> value</option>
<option name=...> value</option>
</select>
在rails中,可以使用 select
的辅助方法.
<%= select_tag options_for_select([1,2,3], default_value ) %>
作业:
- 实现一个form
- form中,包含:
<input type='text'/>
<input type='hidden'/>
<textarea/>
<select>
<options>
<input type='radio'/>
<input type='checkbox'/>
<input type='file'/>
<input type='password'/>
把上面几个学好就可以。
不用学的: 日期输入框,数字, url, phone .. 其他的都不用学。
- 实现一个 select ( 选择框,下拉单, drop down (US 叫法)) 里面的选项, 来自于: 数据库。 举个例子:
3.1 数据库中,有两个表,books(书) , 一个是: publisher (出版社) books : publisher = n : 1
books表, 有下面的列:
id title publisher_id -- | -- | -- 1 | java入门 | 2 | ruby 入门 | 3 | javascript 入门 |
publisher 表只有两个列:
id, name --- | --- 10 | 人民教育出版社 20 | 商务印书馆 30 | 电子工业出版社
3.2 写出下面的 html: (使用 select_tag 与 options_from_collection_for_select )
选择出版社:
<select name='books[publisher_id]' >
<option value='10'>人民教育出版社</option>
<option value='20'>商务印书馆</option>
<option value='30'>电子工业出版社</option>
</select>
3.3 在新建或者 编辑 books的页面中, 可以对publisher 进行选择。
并且,可以在后台(create, update action中)保存。
再进一步的细化提示:
- 创建两个表。 (rails generate migration )
- 有相关的 model ( app/models/book.rb, publisher.rb )
- 查看(实现形式1): select_tag, options_from_collection_for_select
- 查看(实现形式2): select_tag , options_for_select
FormHelper FormTagHelper.
两个例子,来对比说明。
区别:
1.form helper
:
<%= f.text_field :title %>
1.1. 需要与 form object ( <%= form_for @book do |f| %> .. .<% end %>
)配合使用。
1.2 对于生成的<input type='text' name="student[age]"/>
中,
它的name
是自动生成的。 例如: name="student[age]"
student
必须是某个model的实例
并且, age
必须是form object的方法(也就是 数据库的列。)
优点: 可以简化我们对表单项的操作。(例如: 下拉单 或 文本框的 默认值)
2.form tag helper
:
<%= text_field_tag 'my_title' %>
2.1 可以独立使用。 跟表单对象无关。 2.2 名字可以随意取。 name='abc' 优点: 特别灵活。 可以脱离表单对象使用。 而且 便于理解。
相同点: 作用是一样的。
例如:
<%= f.submit "OK"%>
<%= submit_tag "OK" %>
都会生成:
<input type="submit" name="commit" value="OK">
例子2:
<%= f.text_field :title %>
<%= text_field_tag 'article[title]' %>
都会生成:
<input type="text" name="article[title]" id="article_title">
所以说, form helper
与 form tag helper
都是一样的。 一个东西。写法不同。
甚至, 我们在API文档上,都可以看到Rails 作者,告诉我们:
form helper: http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html
form tag helper: http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html
都可以看到, xx_field 的文档中,会说: 请参考 xx_field_tag . 例如:
time_field 中:
Options: Accepts same options as time_field_tag
提示: 我们做rails的前半年,一定要多翻: 上面两个文档。
注意:
api 的文档, 跟真正的用法,有一定区别。 http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-label
api 中写着:
text_field(object, method, options={})
实际问题来了:
(前提: @article
是在 controller中定义好的)
下面,两个input
, 是一样的。
<%= form_for @article do |f| %>
title: <%= f.text_field :title %>
<%= text_field :article, :title %>
end
都会生成:
<input type="text" name="article[title]" id="article_title">
但是,实战当中, 我们都是用 f.text_field('method')
这样的 简写形式。
不会使用 text_field('object', 'method')
这样的形式
API 那样写,是因为在API文档中没有上下文。 所以它那么表示的。(深层会有很多逻辑,很多 动态生成的代码,我们不考虑)
第二个提示(内容与上面的可能重复)
<%= form_for @article do %>
等同于:
两个形式:
1.(@article 是 Article.new的时候)
<%= form_for :article , :url => article_path, :method => 'post' do %>
- (@article 是 Article.find(2)的时候)
<%= form_for :article , :url => update_article_path(:id => 2), :method => 'put' do %>
注意:
- 忘掉
form_for :xx
的形式, 使用form_for @xx
. - 只要当前操作与 数据库有关系,那么就用
form_for
,会让你特别省力。 除非有必要,才 使用form_tag
例如:
form_for
对应: 创建, 更新操作。
form_tag
对应: 某个列表页的搜索。
form_for
与 form_tag
的区别和联系
controller:
@book = Book.new
view中, 下面两个是相同的:
<%= form_for @book do |f|%>
<%= f.text_field :title %>
<% end %>
<%= form_tag '/books' do %>
<%= text_field_tag 'book[title]', '' %>
<% end %>
controller:
@book = Book.find(3)
view中, 下面两个是相同的:
<%= form_for @book do |f|%>
<%= f.text_field :title %>
<% end %>
<%= form_tag '/books/3/update' do %>
<%= text_field_tag 'book[title]', @book.title %>
<% end %>