Introducing MiniForm
My last blog post - Dealing with form objects, got me thinking about form objects. I also had a 12 hours flight to San Francisco, where I was meeting my teammates at Product Hunt.
During that flight, I went back and checked all of my custom form implementations. I gathered them together in a folder and started combining and extracting the common parts. By the end of the flight I almost had what now is MiniForm. (I had to change its name several times since I'm not very good with names and most of the decent names are not available).
MiniForm allows the following code:
class RegistrationForm
include ActiveModel::Model
attr_reader :user, :account
attr_accessor :first_name, :last_name, :email, :name, :plan, :terms_of_service
# user validation
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true, email: true
# account validation
validates :account_name, presence: true
# form custom validation
validates :plan, inclusion: {in AccountPlan::VALUES}
validates :terms_of_service, acceptance: true
# ensure uniqueness
validate :ensure_unique_user_email
validate :ensure_unique_account_name
def initialize
@user = User.new
@account = Account.new owner: @user
end
def update(attributes)
attributes.each do |name, value|
public_send "#{name}=", value
end
if valid? & user.update(user_attributes) && account.update(account_attributes)
account.users << user
AccountWasCreated.perform_later(account)
else
false
end
end
private
def ensure_unique_user_email
errors.add :email, 'already taken' if User.where(email: email).any?
end
def ensure_unique_account_name
errors.add :name, 'already taken' if Account.where(name: name).any?
end
def user_attributes
{first_name: first_name, last_name: last_name, email: email}
end
def account_attributes
{plan: plan, name: name}
end
end
To become:
class RegistrationForm
include MiniForm::Model
# `model` delegates attributes to models
# and copies the validations from them
model :user, attributes: %i(first_name last_name email), save: true
model :account, attributes: %i(name plan), save: true
attributes :terms_of_service
validates :plan, inclusion: {in AccountPlan::VALUES}
validates :terms_of_service, acceptance: true
def initialize
@user = User.new
@account = Account.new owner: @user
end
# `update` calls `perform` if model is valid
# and models are saved
def perform
account.users << user
AccountWasCreated.perform_later(account)
end
end