HyperActiveForm is a simple form object implementation for Rails.
Form objects are objects that encapsulate form logic and validations, they allow to extract the business logic out of the controller and models into specialized objects.
HyperActiveForm's form objects mimic the ActiveModel API, so they work out of the box with Rails' form helpers, and allow you to use the ActiveModel validations you already know.
This allows you to only keep strictly necessary validations in the model, and have business logic validations in the form object. This is especially useful when you want different validations to be applied depending on the context.
Add this line to your application's Gemfile:
gem 'hyperactiveform'
And then execute:
$ bundle install
Run the install generator:
$ rails generate hyper_active_form:install
this will create an ApplicationForm
class in your app/forms directory. You can use it as a base class for your form objects.
You can generate a form and its tests with the following command:
$ rails generate form FooBar
This will create the FooBarForm
Here is an example of an HyperActiveForm
form object:
class ProfileForm < ApplicationForm
# proxy_for is used to delegate the model name to the class, and some methods to the object
# this helps use `form_with` in views without having to specify the url
proxy_for User, :@user
# Define the form fields, using ActiveModel::Attributes
attribute :first_name
attribute :last_name
attribute :birth_date, :date
# Define the validations, using ActiveModel::Validations
validates :first_name, presence: true
validates :last_name, presence: true
validates :birth_date, presence: true
# Pre-fill the form if needed
def setup(user)
@user = user
self.first_name = user.first_name
self.last_name = user.last_name
self.birth_date = user.birth_date
end
# Perform the form logic
def perform
@user.update!(
first_name: first_name,
last_name: last_name,
birth_date: birth_date
)
end
end
The controller would look like this:
class UsersController < ApplicationController
def edit
@form = ProfileForm.new(user: current_user)
end
def update
@form = ProfileForm.new(user: current_user)
if @form.submit(params[:user])
redirect_to root_path, notice: "Profile updated"
else
render :edit, status: :unprocessable_entity
end
end
And the view would look like this:
<%= form_with(model: @form) do |f| %>
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
<%= f.date_field :birth_date %>
<%= f.submit %>
<% end %>
HyperActiveForm
mimics a model object, you can use proxy_for
to tell it which class and object to delegate to.
When using form_for
or form_with
, Rails will choose the URL and method based on the object, according to the persisted state of the object and its model name.
The first argument of proxy_for
is the class of the object, and the second argument is the name of the instance variable that holds the object.
class ProfileForm < ApplicationForm
proxy_for User, :@user # Will delegate to @user
end
If you pass an url and method yourself, you don't need to use proxy_for
.
setup
is called just after the form is initialized, and is used to pre-fill the form with data from the object.
setup
will receive the same arguments as the initializer, so you can use it to pass any data you need to the form.
class ProfileForm < ApplicationForm
def setup(user)
@user = user
self.first_name = user.first_name
self.last_name = user.last_name
self.birth_date = user.birth_date
end
end
When using submit
or submit!
, HyperActiveForm
will first assign the form attributes to the object, then perform the validations, then call perform
on the object if the form is valid.
The perform
method is where you should do the actual form logic, like updating the object or creating a new one.
If the return value of perform
is not truthy, HyperActiveForm
will consider the form encountered an error and submit
will return false
, or submit!
will raise a HyperActiveForm::FormDidNotSubmitError
.
At any point during the form processing, you can raise HyperActiveForm::CancelForm
to cancel the form submission, this is the same as returning false
.
HyperActiveForm
provides a method to add errors from a model and apply them fo the form.
This is useful when the underlying model has validations that are not set up in the form object, and you want them to be applied to the form.
class User < ApplicationRecord
validates :first_name, presence: true
end
class ProfileForm < ApplicationForm
proxy_for User, :@user
attribute :first_name
def setup(user)
@user = user
self.first_name = user.first_name
end
def perform
@user.update!(first_name: first_name) || add_errors_from(@user)
end
end
The power of HyperActiveForm
is that you can use it to create forms that don't map to a single model.
Some forms can be used to create several models at once. Doing so without form objects can be tedious especially with nested attributes.
Some forms dont map to any model at all, like a simple contact form that only sends an email and saves nothing in the database, or a sign in form that would only validate the credentials and return the instance of the connected user.
One great example of such forms are search forms. You can use a form object to encapsulate the search logic :
class UserSearchForm < ApplicationForm
attribute :name
attribute :email
attribute :min_age, :integer
attr_reader :results # So the controller can access the results
def perform
@results = User.all
if name.present?
@results = @results.where(name: name)
end
if email.present?
@results = @results.where(email: email)
end
if age.present?
@results = @results.where("age >= ?", age)
end
true
end
end
And in the controller:
class UsersController < ApplicationController
def index
@form = UserSearchForm.new
@form.submit!(params[:user])
@users = @form.results
end
end
HyperActiveForm provides callbacks for assign_form_attributes
and submit
.
You can use these callbacks to run code before or after assigning the form attributes or before or after submitting the form.
class ProfileForm < ApplicationForm
# ...
before_submit :do_something_before_submit
before_assign_form_attributes :do_something_before_assign_form_attributes
def do_something_before_submit
# Do something before submitting the form
end
def do_something_before_assign_form_attributes
# Do something before assigning the form attributes
end
end