分享 使用 Form Objects 处理结构复杂的表单

jesktop · 2015年02月09日 · 最后由 JeskTop 回复于 2015年02月10日 · 3372 次阅读

参考:RailsCasts 416-form-objects
自从使用 Form Objects 处理复杂的表单后,我就不想在考虑使用 Rails 带的那套方法了。
原来的方法是,如果一个 Form 需要同时处理两个 Model 的数据时,就需要考虑使用accepts_nested_attributes_for,来建立数据间的关系。但是如果同时需要处理三个或三个以上的 Model 数据时,就会变得很混乱,form 页面需要各种嵌套,然后 model 层需要小心翼翼的设置accepts_nested_attributes_for
但是自从学会了 Form Objects 后,妈妈在也不担心我写复杂的表单了。

模拟场景

模拟一个现实中的场景,我们需要一个 Form 处理三个 model 的事情,这里先介绍即将登场的三位 Model:

Class Product  # 产品
  has_many :product_prices
  belongs_to :product_color
end
Class ProductPrice  # 产品价格(多种价格)
  belongs_to :product
  enum category: { sell: 0, cost: 1} 
end
Class ProductColor  # 产品颜色
  has_many :products
end

Form 里的操作是,当我创建一个 Product 时,需要同时添加多个 Product Price(销售价和成本价),和关联一个 Product Color(如果颜色存在,则直接进行关联,不存在就重新创建)。
所以我们需要设计四个 input:name, sell_price, cost_price, color;别看就这么简单的四个玩意,其实是同时涉及三张表的操作哦。

Form 的设计

这里谈的设计不是指怎么美化这个表单,这里我就不谈如何美化表单的事情了,简单的使用 simple_form 来处理这个表单把,接招:

= simple_form_for @product, url: products_path, method: :post, html: {class: 'form-horizontal'} do |f| 
  = f.input :name
  = f.input :sell_price
  = f.input :cost_price
  = f.input :color

  .form-actions
    = f.button :submit, class: 'btn btn-primary', value: "提交"

请先不要着急着去看这个 form 究竟长什么样了,如果你跑去看,估计就是满满的错误,因为@product还没定义呢!
当然有同鞋们会说,@product不就是@product = Product.new吗?当然不是!!因为 Product 没有 sell_price 等字段啊!

建立 Form Objects

好了,主菜来了!首先我们在app/forms/里建一个products_create_form.rb文件,然后咱们来看看应该如何完善。

使用 ActiveModel::Model
Class ProductsCreateForm
  include ActiveModel::Model

  def self.model_name
    ActiveModel::Name.new(self, nil, "Product")
  end

  class << self
    def i18n_scope
      :activerecord
    end
  end
end

为什么在这里要include ActiveModel::Model呢?因为这样可以使用大部分我们在 Model 中常用到的方法,这里我们主要要使用validates去验证必填项。
然后另外self.model_name这里把 ActiveModel::Name 设置为'Product了,那生成的form中input的name就是product[name]。 而i18n_scope`则是让你把对于 model 来说的 i18n 设置放回 activerecord 中,不然要设置 Form Objects 下的 i18n 就需要针对 ActiveModel 来写 i18n 信息,这个就主要看个人需求是怎么样,一般我都会加上。

完善 Form Objects

然后我们需要补全剩余的信息了:

Class ProductsCreateForm
  include ActiveModel::Model

  def self.model_name
    ActiveModel::Name.new(self, nil, "Product")
  end

  class << self
    def i18n_scope
      :activerecord
    end
  end

  attr_accessor :name, :sell_price, :cost_price, :color

  validates :name, :sell_price, :cost_price, :color, presence: true

  def initialize
  end

  def submit(params)
    self.name = params[:name]
    self.sell_price = params[:sell_price]
    self.cost_price = params[:cost_price]
    self.color = params[:color]
    if valid?
      product_color = ProductColor.where(name: self.color).first_or_create
      product = Product.create(name: self.name, product_color: product_color)
      ProductPrice.create(category: 'sell', value: self.sell_price, product: product)
      ProductPrice.create(category: 'cost', value: self.cost_price, product: product)
      true
    else
      false
    end
  end
end

在 attr_accessor 中加入我们需要输入的参数,并且把必填项加入到 validates 中。在 submit 时,valid?方法就会生效了。这一个 create 的流程下来,还是非常简单的。

调用 ProductsCreateForm

所以在刚刚@product中,调用 ProductsCreateForm 就可以了:

@product = ProductsCreateForm.new

而在 create 方法时,也是非常的简单的:

@product = ProductsCreateForm.new
if @product.submit(params[:product])
  ...
else
  ...
end

整个流程下来,controller 的代码已经非常简单了,把需要用的逻辑单独放在 ProductsCreateForm 中,不需要放到 Model 中,避免 Model 的臃肿,而且非常直观。
那么问题来了,如果我是需要 Update 这些信息,那么直接调用 ProductsCreateForm 可以吗?显然那是不行的,Update 的话会比较复杂些。

使用 Form Objects 更新信息

我们重新建一个ProductUpdateForm来专门处理 Update:

Class ProductsUpdateForm
  include ActiveModel::Model

  def self.model_name
    ActiveModel::Name.new(self, nil, "Product")
  end

  class << self
    def i18n_scope
      :activerecord
    end
  end

  validates :name, :sell_price, :cost_price, :color, presence: true

  def initialize product
    @product = product
    @product_sell_price = @product.product_price.find_by(category: ProductPrice::categories[:sell])
    @product_cost_price = @product.product_price.find_by(category: ProductPrice::categories[:cost])
  end

  def name
    @name ||= @product.name
  end

  def sell_price
    @sell_price ||= @product_sell_price.value
  end

  def cost_price
    @cost_price ||= @product_cost_price.value
  end

  def color
    @color ||= @product.product_color.name
  end

  def update(params)
    @name = params[:name]
    @sell_price = params[:sell_price]
    @cost_price = params[:cost_price]
    @color = params[:color]
    if valid?
      @product.update(name: self.name)
      @product_sell_price.update(value: self.sell_price)
      @product_cost_price.update(value: self.cost_price)
      @product.product_color.update(name: self.color)
      true
    else
      false
    end
  end
end

相比 Create 来说,主要不同的地方也就是每个参数的定义方式不在使用attr_accessor,因为参数已经不会为空的,而需要从数据库中读取。然后别的相比之下差别也不大,我这里就不另外做解释了,如果还想了解多一些,可以看看 RailsCasts 的视频,非常不错的哦。

原文: 使用 Form Objects 处理结构复杂的表单

不错的解决手段。

有点觉得 Update 解决的不太优雅。

本来想对照这篇文章把一个页面给改成这样的处理,结果发现那个页面需要操作两表共计快 30 个字段 attr_accessor 要写好长一段。。。

#1 楼 @lyfi2003 或者可以对 Update 的解决方法进行优化。 #2 楼 @ywjno 这确实是个问题,但目前我还没试过一个表单涉及 30 个字段的情况呢。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号