原文链接: 使用 rails 自带的 Form Builder 来重构你的 Form
开发一个项目,你可能难以避免使用到表单,特别是后台项目的增删改查,你就可能使用得更多。
这些表单,可能内容和格式都差不多,或许你就用 bootstrap 这样组件来格式化你的表单。
有时候,你要写好多代码,且是重复的,来看下下面的代码:
<%= form_for [:admin, @executive], html: { class: 'form-horizontal form-label-left', "data-parsley-validate" => true } do |f| %>
<% if @executive.errors.any? %>
<div id="error_explanation"></div>
<h3><%= "#{@executive.errors.count}个错误:" %></h3>
<ul>
<% @executive.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<div class="form-group">
<label class="col-sm-2 control-label">名称</label>
<div class="col-sm-10"><%= f.text_field :name, required: 'required', class: 'form-control' %></div>
</div>
<% if params[:level] || @executive.parent_id.present? %>
<div class="form-group">
<label class="col-sm-2 control-label">所属产品系列</label>
<div class="col-sm-10"><%= f.collection_select :parent_id, @executives, :id, :name, { include_blank: "选择产品系列" }, { class: 'form-control', required: 'required'} %></div>
</div>
<% end %>
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<%= f.submit value: '提交', class: 'btn btn-primary' %>
</div>
</div>
<% end %>
这样大约有 31 行代码,可以把它精简成 10 行 (有空行) 那样。
除去form_for
那行,先不看,来看第一部分,就是显示错误的,如果表单提交不成功,把错误信息显示出来。
<% if @executive.errors.any? %>
<div id="error_explanation"></div>
<h3><%= "#{@executive.errors.count}个错误:" %></h3>
<ul>
<% @executive.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
类似这样的代码,有时候换了另一个表对象,你又要重新写一次:
<% if @category.errors.any? %>
<div id="error_explanation"></div>
<h3><%= "#{@category.errors.count}个错误:" %></h3>
<ul>
<% @category.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
只是把@executive
换成了@category
而已,却要写这么多行代码,就算是复制,看着也不爽,能不能把它们都简化呢?
能的,像上面刚才那样的代码,我只需要一行。
包括下面的代码:
<div class="form-group">
<label class="col-sm-2 control-label">名称</label>
<div class="col-sm-10"><%= f.text_field :name, required: 'required', class: 'form-control' %></div>
</div>
这样的代码结构都差不多,只是不同的字段在换而已。
统统简化一下。
我们不需要使用第三方的插件,rails 默认就有这样的功能,让你重新修改系统的 tag 或重新定义自己的 tag。
先在app
下新建一个目录叫form_builders
,再在里面新建一个文件叫bootstrap_form_builder.rb
。
内容如下:
# app/form_builders/bootstrap_form_builder.rb
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
delegate :content_tag, to: :@template
def error_messages
if object && object.errors.any?
content_tag(:div, id: 'error_explanation') do
content_tag(:h3, "#{object.errors.count}个错误") +
content_tag(:ul) do
object.errors.full_messages.map do |msg|
content_tag(:li, msg)
end.join.html_safe
end
end
end
end
end
在config/application.rb
文件中加入这行配置:
config.autoload_paths << Rails.root.join('app', 'form_builders')
按照#10 楼的建议,在 rails 5 中这里需要把
autoload_paths
改成eager_load_paths
。
再到app/helpers/application_helper.rb
文件中添加一个方法。
module ApplicationHelper
def bootstrap_form_for(object, options = {}, &block)
options[:builder] = BootstrapFormBuilder
form_for(object, options, &block)
end
end
接下来就可以使用bootstrap_form_for
这个方法来代替默认的form_for
方法了。
<%= bootstrap_form_for [:admin, @executive], html: { class: 'form-horizontal form-label-left', "data-parsley-validate" => true } do |f| %>
<%= f.error_messages %>
...
<% end %>
看到没有,之前显示错误信息那里是好几行代码的,现在被简化成了一行代码。
我们接下来把显示名称
和所属产品系列
这两个地方也简化一下。
在app/form_builders/bootstrap_form_builder.rb
文件中,再增加一些内容。
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
delegate :content_tag, to: :@template
%w( text_field text_area url_field file_field collection_select select ).each do |method_name|
define_method(method_name) do |method, *tag_value|
content_tag(:div, class: 'form-group') do
label(method, class: 'col-sm-2 control-label') +
content_tag(:div, class: 'col-sm-10') do
super(method, *tag_value)
end
end
end
end
def error_messages
...
end
现在就可以只用<%= f.text_field :name, required: 'required', class: 'form-control' %>
来代替之前的好多行代码。
把那些多余的div
,class
删除掉即可。
这个可以看下面最后的结果。
同样的提交按钮,也能处理,在BootstrapFormBuilder
中增加下面这个方法:
def submit(*tag_value)
content_tag(:div, class: 'form-group') do
content_tag(:div, class: 'col-sm-4 col-sm-offset-2') do
super
end
end
end
回到前面。
<%= form_for [:admin, @executive], html: { class: 'form-horizontal form-label-left', "data-parsley-validate" => true } do |f| %>
这里有好多 class,都是一样的,我也不想重复,想改成这样写。
<%= bootstrap_form_for [:admin, @executive] do |f| %>
到 helper 方法中,修改一下。
def bootstrap_form_for(object, options = {}, &block)
options[:html] ||= {}
options[:html][:class] = 'form-horizontal form-label-left'
options[:html]["data-parsley-validate"] = true
options[:builder] = BootstrapFormBuilder
form_for(object, options, &block)
end
最后精简后的代码可能是这样的子:
<%= bootstrap_form_for [:admin, @executive] do |f| %>
<%= f.error_messages %>
<%= f.text_field :name, required: 'required', class: 'form-control' %>
<% if params[:level] || @executive.parent_id.present? %>
<%= f.collection_select :parent_id, @executives, :id, :name, { include_blank: "选择产品系列" }, { class: 'form-control', required: 'required'} %>
<% end %>
<div class="hr-line-dashed"></div>
<%= f.submit value: '提交', class: 'btn btn-primary' %>
<% end %>
比之前精简多了。
这样的代码又不影响正常的 form_for,你还可以继续使用默认的 form_for 下的各种 tag,没有影响,当你要使用自己定义的 form_for,只要改成自己定义的就可以了。
你的应用可能有好多图片编辑的功能,比如下面这样:
编辑的时候需要显示图片。
你可能会这样写:
<% if @product.image_url.present? %>
<div class="form-group">
<label class="col-sm-2 control-label"></label>
<div class="col-sm-10">
<%= image_tag @product.image_url(:product), width: '100', height: '100' %>
</div>
</div>
<% end %>
<div class="form-group">
<label class="col-sm-2 control-label">图片<span class="required">*</span></label>
<div class="col-sm-10">
<%= f.file_field :image, required: (@product.new_record? ? true : false), class: 'form-control' %>
</div>
</div>
我把它精简成了一行
<%= f.image_file_field :image, required: (@product.new_record? ? true : false), class: 'form-control' %>
按照#6 楼的建议
@product.new_record? ? true : false
改成@product.new_record?
会好点。
参考一下我的写法。
还是到bootstrap_form_builder.rb
文件中增加下面几行:
def image_file_field(method, *tag_value)
image_method = "#{method}_url".to_sym
image_cache = "#{method}_cache".to_sym
if object.send(image_method)
image_version = tag_value.first[:image_version].to_sym if tag_value.first[:image_version].present?
content_tag(:div, class: 'form-group') do
label(method, " ".html_safe, class: 'col-sm-2 control-label') +
content_tag(:div, class: 'col-sm-10') do
image_tag object.send(image_method, image_version), width: '100', height: '100'
end
end +
file_field(method, *tag_value) + hidden_field(image_cache)
else
file_field(method, *tag_value)
end
end
可能你运行的时候会提示报错,提示没有image_tag
这个方法,没关系。
改一下最前面的delegate
方法。
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
delegate :content_tag, :image_tag, to: :@template
...
end
缺少啥 helper 方法或 tag 方法,就在delegate
后面添加就好了。
对了,最后你可能会发现结果表单中的字段名怎么变成英文输出了,这没关系,你加上 i18n 国际化就好了,会全中文的。
完结。