Creating Form Objects with ActiveModel and gem Virtus
Bài đăng này đã không được cập nhật trong 8 năm
Khi bạn muốn update nhiều ActiveRecord models chỉ trong một lần submit form, thì thường thường chúng ta sẽ dùng "accepts_nested_attributes_for". Những ai sử dụng "accepts_nested_attributes_for" thì cũng biết sự khó khăn của nó đem lại.
Một giải pháp thay thế cho việc này là sử dụng "form object", Form Object có thể đóng gói rất tốt những thực thi này. Ngoài ra Fomr Object còn giảm thiểu được tình trạng "fat models", validates dễ dàng hơn, giúp code nhìn sáng sủa hơn. Giờ chúng ta sẽ bắt tay vào làm một ví dụ nhỏ nhé.
Sử dụng Gem Virtus and ActiveModel để tạo form objects
Gem Virtus cho phép bạn định nghĩa các thuộc tính trên classes, modules hoặc các class instances với các cài đặt không bắt buộc như type, phạm vi read/write các method.
Giờ giả sử ta có 1 bảng users và 1 bảng expenses. khi ta update vào bảng users thì cũng sẽ cập nhật vào bảng expenses.
b1: trong folder app tạo 1 thư mục form
b2: tạo 1 class UserExpenseForm trong thư mục form vừa tạo.
# app/forms/user_expense_form.rb
class UserExpenseForm
include ActiveModel::Model
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
# Attributes (DSL provided by Virtus)
attribute :email, String
attribute :amount, Integer
attribute :paid, Boolean, default: false)
# Access the expense record after it's saved
attr_reader :expense
# Validations
validates :email, presence: true
validates :amount, numericality: { only_integer: true, greater_than: 0 }
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.create!(email: email)
@expense = user.expenses.create!(amount: amount, paid: paid)
end
end
trong class trên đã tạo 2 database records trong 2 table khác nhau trong cùng 1 form. Ta có thể validates tất cả trong cùng form này.
b3. Trong controller ta sẽ gọi tới form này
# app/controller/expenses_controller.rb
class ExpensesController < ApplicationController
def new
@user_expense_form = UserExpenseForm.new
end
def create
@user_expense_form = UserExpenseForm.new(user_expense_form_params)
if @user_expense_form.save
redirect_to dashboard_url, notice: "Expense ID #{@user_expense_form.expense.id} has been created"
else
render :new
end
end
private
# Using strong parameters
def user_expense_form_params
params.require(:user_expense_form).permit!(:email, :amount, :paid)
end
end
b4: Trong phần view ta vẫn sử dụng form_for bình thường với object là @user_expense_form ta đã khai báo trong controller.
<%= form_for @user_expense_form, url: expenses_path do |f| %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.label :amount %>
<%= f.number_field :amount %>
<%= f.label :paid %>
<%= f.check_box :paid %>
<%= f.submit %>
<% end %>
Need i18n support?
Câu trả lời ở đây là có, vì chúng ta đã
include ActiveModel::Model
nên việc thao tác ở đây giống như thao tác với ActiveModel::Model.
Trên đây chúng ta đã có ví dụ để tạo 1 form object. Mô hình này sẽ hợp lí với những form đơn giản thay cho việc dùng "accepts_nested_attributes_for". Đối với form phưc tạp thì ta cần kết hợp với một số kỹ thuật khác.
**Tham Khảo **
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ http://webuild.envato.com/blog/creating-form-objects-with-activemodel-and-virtus/
Cảm ơn các bạn đã theo dõi.
All rights reserved