世界上最伟大的投资就是投资自己的教育

首页Ruby
随风 · 练气

使用 Rails 自带的 Form Builder 来重构你的 Form

随风发布于4526 次阅读

1. 介绍

开发一个项目,你可能难以避免使用到表单,特别是后台项目的增删改查,你就可能使用得更多。

这些表单,可能内容和格式都差不多,或许你就用 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>

这样的代码结构都差不多,只是不同的字段在换而已。

统统简化一下。

2. 使用 Form Builder

我们不需要使用第三方的插件,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')

再到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' %>来代替之前的好多行代码。

把那些多余的divclass删除掉即可。

这个可以看下面最后的结果。

同样的提交按钮,也能处理,在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,只要改成自己定义的就可以了。

3. 加一个额外的功能

你的应用可能有好多图片编辑的功能,比如下面这样:

编辑的时候需要显示图片。

你可能会这样写:

<% 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' %>

参考一下我的写法。

还是到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, "&nbsp;".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 国际化就好了,会全中文的。

完结。

本站文章均为原创内容,如需转载请注明出处,谢谢。

0 条回复
暂无回复~~
喜欢
统计信息
    学员: 29915
    视频数量: 1996
    文章数量: 526

© 汕尾市求知科技有限公司 | Rails365 Gitlab | 知乎 | b 站 | csdn

粤公网安备 44152102000088号粤公网安备 44152102000088号 | 粤ICP备19038915号

Top