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", 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