参考: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;别看就这么简单的四个玩意,其实是同时涉及三张表的操作哦。
这里谈的设计不是指怎么美化这个表单,这里我就不谈如何美化表单的事情了,简单的使用 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 等字段啊!
好了,主菜来了!首先我们在app/forms/
里建一个products_create_form.rb
文件,然后咱们来看看应该如何完善。
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 信息,这个就主要看个人需求是怎么样,一般我都会加上。
然后我们需要补全剩余的信息了:
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 的流程下来,还是非常简单的。
所以在刚刚@product中,调用 ProductsCreateForm 就可以了:
@product = ProductsCreateForm.new
而在 create 方法时,也是非常的简单的:
@product = ProductsCreateForm.new
if @product.submit(params[:product])
...
else
...
end
整个流程下来,controller 的代码已经非常简单了,把需要用的逻辑单独放在 ProductsCreateForm 中,不需要放到 Model 中,避免 Model 的臃肿,而且非常直观。
那么问题来了,如果我是需要 Update 这些信息,那么直接调用 ProductsCreateForm 可以吗?显然那是不行的,Update 的话会比较复杂些。
我们重新建一个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 的视频,非常不错的哦。