Form Strategy
The form strategy provides a declarative way to validate user input using form objects. It bridges the gap between raw user input (like params) and validated, structured data.
When to Use
Use the form strategy when you need to validate user-facing input with user-friendly error messages. This is different from expects validations, which validate the developer contract (how the action is called).
Basic Usage
class CreateUser
include Axn
use :form, type: CreateUser::Form
def call
# form is automatically validated and exposed
# If validation fails, the action fails with form.errors
User.create!(form.to_h)
end
end
class CreateUser::Form < Axn::FormObject
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2 }
endConfiguration Options
The form strategy accepts several configuration options:
| Option | Default | Description |
|---|---|---|
type | Auto-detected | The form class to use (see Type Resolution) |
expect | :params | The input field name to read from |
expose | :form | The field name to expose the form object as |
inject | nil | Additional context fields to inject into the form |
Type Resolution
The type option determines which form class to use:
- Explicit class:
use :form, type: MyFormClass - String constant path:
use :form, type: "CreateUser::Form" - Auto-detected: If not specified, inferred from action name + expose name (e.g.,
CreateUser+:form→CreateUser::Form)
# Explicit type
use :form, type: RegistrationForm
# String constant (useful for avoiding load order issues)
use :form, type: "Users::RegistrationForm"
# Auto-detected from action name
class CreateUser
include Axn
use :form # Uses CreateUser::Form
endInline Form Definition
You can define the form class inline using a block:
class CreateUser
include Axn
use :form do
validates :email, presence: true
validates :name, presence: true
end
def call
User.create!(form.to_h)
end
endThis creates an anonymous form class that inherits from Axn::FormObject.
Custom Field Names
class ProcessOrder
include Axn
# Read from :order_params, expose as :order_form
use :form, expect: :order_params, expose: :order_form, type: OrderForm
def call
# Access via order_form instead of form
Order.create!(order_form.to_h)
end
endInjecting Context
Use inject to pass additional context fields to the form:
class UpdateProfile
include Axn
expects :user, model: User
use :form, type: ProfileForm, inject: [:user]
def call
user.update!(form.to_h)
end
end
class ProfileForm < Axn::FormObject
attr_accessor :user # Injected from action context
validates :email, presence: true
validate :email_unique_for_other_users
private
def email_unique_for_other_users
return if user.nil?
return unless User.where.not(id: user.id).exists?(email: email)
errors.add(:email, "is already taken")
end
endHow It Works
When you use the form strategy, the following happens automatically:
- Expects params: Adds
expects :params, type: :params(or your customexpectfield) - Exposes form: Adds
exposes :form(or your customexposefield) - Creates form: Defines a memoized method that creates the form from params
- Validates in before hook: Runs
form.valid?in a before hook; if invalid, the action fails
# This:
use :form, type: MyForm
# Is roughly equivalent to:
expects :params, type: :params
exposes :form, type: MyForm
def form
@form ||= MyForm.new(params)
end
before do
expose form: form
fail! unless form.valid?
endError Handling
When form validation fails:
- The action fails (returns
ok? == false) result.errorcontains a generic messageresult.form.errorscontains the detailed validation errors
result = CreateUser.call(params: { email: "", name: "" })
result.ok? # => false
result.form.errors.full_messages
# => ["Email can't be blank", "Name can't be blank"]User-Facing Errors
To expose user-friendly error messages, configure a custom error handler:
class CreateUser
include Axn
use :form, type: CreateUser::Form
error { form.errors.full_messages.to_sentence }
def call
User.create!(form.to_h)
end
endComplete Example
class CreateCompanyMember
include Axn
expects :company, model: Company
use :form, type: MemberForm, inject: [:company]
exposes :member
error { form.errors.full_messages.to_sentence }
success { "#{member.name} has been added to #{company.name}" }
def call
member = company.members.create!(form.to_h)
expose member: member
end
end
class MemberForm < Axn::FormObject
attr_accessor :company # Injected
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true
validates :role, presence: true, inclusion: { in: %w[admin member guest] }
validate :email_not_already_member
private
def email_not_already_member
return unless company&.members&.exists?(email: email)
errors.add(:email, "is already a member of this company")
end
endSee Also
- Axn::FormObject - The base class for form objects
- Validating User Input - When to use form validation vs expects validation