Creating Form Objects with ActiveModel and gem Virtus
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", ...
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.