Form objects are an additional layer between the controller and the ActiveRecord model.
This layer maps a specific form to the concerned model and contains form-specific business logic (validations, virtual attributes, persistence of other depending ActiveRecord objects).
This is how the ActiveRecord model can be relieved of some responsibilities:
- Callbacks / observer, which also modify and persist other model objects
- Context-dependent model validations (conditional validations with if - else)
- Virtual ActiveRecord accessor methods, which are used only for caching form values
- ActiveRecord models with dependencies to external systems (e.g., mailers)
By having form objects address those tasks, ActiveRecord objects can deal with their main responsibility: CRUD.
The controller
An example is the bank account registration. An address can also be specified during registration.
The Models:
rails g model BankAccount iban:string address:references
rails g model Address name:string family_name:string street:string zip:string
The controller meets a standard controller. The create action accepts the request, calls the business logic (in this case the form object) and responds with JSON:
# app/controllers/bank_accounts_controller.rb
class BankAccountsController < ApplicationController
def create
@bank_account = BankAccountRegistration.new params.require(:bank_account).permit!
if @bank_account.save
render nothing: true, status: :created
else
render json: { errors: @bank_account.errors }, status: :bad_request
end
end
end
The only noteworthy is the missing strong parameters definition. The form object does only pass through the form attributes that are necessary anyway.
The form object
The form object itself corresponds to ActiveModel. Moreover it contains:
- Getter and setter methods for the form fields input
- Validations
- A persistence method save
The save method includes the necessary business logic. After the form input validation, the database transaction starts like:
# app/forms/bank_account_registration.rb
class BankAccountRegistration
include ActiveModel::Model
attr_accessor :iban,
:name,
:family_name,
:street,
:zip
validates :iban, :name, :family_name,
presence: true
def save
return false if invalid?
bank_account.transaction do
bank_account.address = address
bank_account.save!
# do more stuff like mailer ...
end
end
private
def bank_account
@bank_account ||= BankAccount.new iban: iban
end
def address
@address ||= Address.new name: name,
family_name: family_name,
street: street,
zip: zip
end
end
One of the form objects advantages is their great testability in isolation. If complexity increases, form objects can deal with it very well without much additional effort.
A simple curl request to the form object with the response:
curl -X POST -d 'bank_account[iban]=DE450123456789' localhost:3000/bank_accounts
{"errors":{"name":["can't be blank"],"family_name":["can't be blank"]}}