class UserRegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email, :string
attribute :name, :string
attribute :password, :string
attribute :password_confirmation, :string
attribute :bio, :string
attribute :accept_terms, :boolean
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2 }
validates :password, presence: true, length: { minimum: 8 }
validates :password_confirmation, presence: true
validates :accept_terms, acceptance: true
validate :passwords_match
def save
return false unless valid?
ActiveRecord::Base.transaction do
user = create_user
create_profile(user)
send_welcome_email(user)
user
end
rescue ActiveRecord::RecordInvalid => e
errors.add(:base, e.message)
false
end
private
def passwords_match
if password != password_confirmation
errors.add(:password_confirmation, "doesn't match password")
end
end
def create_user
User.create!(
email: email,
name: name,
password: password
)
end
def create_profile(user)
user.create_profile!(bio: bio)
end
def send_welcome_email(user)
UserMailer.welcome(user).deliver_later
end
end
# Controller usage:
# def create
# @form = UserRegistrationForm.new(registration_params)
#
# if @form.save
# redirect_to root_path, notice: "Welcome!"
# else
# render :new
# end
# end
class OrderForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :user_id, :integer
attribute :shipping_address, :string
attribute :billing_address, :string
attribute :items, default: []
validates :user_id, presence: true
validates :shipping_address, presence: true
validates :billing_address, presence: true
validate :items_present
def initialize(attributes = {})
super
@items ||= []
end
def add_item(product_id:, quantity:, price:)
@items << { product_id: product_id, quantity: quantity, price: price }
end
def save
return false unless valid?
ActiveRecord::Base.transaction do
order = create_order
create_order_items(order)
create_shipping_address(order)
order
end
rescue => e
errors.add(:base, e.message)
false
end
def total_amount
items.sum { |item| item[:quantity] * item[:price] }
end
private
def items_present
errors.add(:items, "must have at least one item") if items.empty?
end
def create_order
Order.create!(
user_id: user_id,
total_amount: total_amount,
status: 'pending'
)
end
def create_order_items(order)
items.each do |item|
order.order_items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
price: item[:price]
)
end
end
def create_shipping_address(order)
order.create_shipping_address!(
address: shipping_address,
address_type: 'shipping'
)
unless shipping_address == billing_address
order.create_billing_address!(
address: billing_address,
address_type: 'billing'
)
end
end
end
Form objects encapsulate form logic separate from models. I use form objects for multi-model forms, complex validations, or forms not directly mapping to models. Form objects include ActiveModel modules for validations and callbacks. They handle parameter whitelisting, validation, and persistence. Form objects keep models focused on business logic, controllers thin. They're especially useful for wizard-style forms spanning multiple steps. Testing form objects is straightforward—no database setup needed for validation tests. Form objects improve code organization in complex applications. Following Single Responsibility Principle, they make forms maintainable and testable.