Why?

In a development day-to-day basis, we commonly are confronted with situations where we need to save data in more than one database table, running it's own validations, and the validations of the all context together. In most cases a Form Object is a perfect solution to deliver those records in a fun and maintenable code.

Actually, there's a lot of another gems to do that, like the great reform or activeform-rails, which are a more complete solution for this problem. But, if you are looking for something lighter, maybe this gem could fit well for you.

Installation

Add this line to your application's Gemfile:

gem 'base_form'

And then execute:

$ bundle

Or install it yourself as:

$ gem install base_form

Usage

Let's suppose you want to create a signup form (you can check this example in the dummy app on this gem specs), with receiving a user email, a user password, a user password confirmation, and a plan. In your signup form, you need to create an account for this user, associate it to a entrance plan if it's not given, and make this user as an owner of this recently created account. Of course, in this case, a simple user model saving will not be sufficient to save all those data and accomplish with the requested business logic. So, you go to the Form Objects way, installs this gem, creates your Ruby class to handle all this logic:

class SignupForm < BaseForm::Form

end

With this empty class created, the easy way to start may be adding the attributes expected in this form, like:

class SignupForm < BaseForm::Form
  attribute :email
  attribute :password
  attribute :password_confirmation
  attribute :plan, Plan, default: proc { Plan.default }
end

Note, if you don't specifies a Plan to this form, it will call a default value, which in this case is calling a proc that will call a default method in Plan model and probably this will return the default plan instance, fetched from the database or something like that.

Let's put some form specific validation here. For example, we don't want the Plan being forced with an empty string for example:

class SignupForm < BaseForm::Form
  # ... attributes, validations ...

  validates :plan, presence: true
end

Now you may be asking: What about email and password? Shouldn't they be validated as well? Well, you could, in fact, add all validations in this form instead put it in your models, but sometimes you don't have much control of that. Then, I'm showing here the case that User model has those validations. Don't be mad ok? :)

The form validations are the first validations tha are performed before it try to persist something here. If this validation fails, for an example, the persist method will not even be called, and we're done with it. Otherwise, it wil try to persist your logic, which we'll implement next.

Ok, now, you need to set the records that you will persist here. In this case is the :user you want to save, and the :account you will want to associate to this user. So, you add it there (I recommend you let this in the top of the class to make it clear):

class SignupForm < BaseForm::Form
  use_form_records :user, :account

  # ... attributes, validations ...
end

This line will automatically generate attr_readers to each record there, and will add these symbols in an array called form_records in your class. To understand it better, let's talk about the persist implementation itself.

By the rule, the persist method is obligatory, and not implementing it, will cause your form raise a NotImplementedError when calling save to it.

All things written inside persist method will automatically run in a ActiveRecord transaction, and if some record have its validation failed, this will perform a rollback and deliver the form to you with those errors grouped through errors method, like any AR model you are already familiar with.

Let me stop to talk and show you something we can call as implementation of this:

class SignupForm < BaseForm::Form
  # form records, attributes, validations, whatever

  private # because isolation is still a necessary evil ;)

  def persist
    @account ||= Account.create plan: plan
    @user ||= account.users.create user_params
  end

  def user_params
    {
      email: email,
      password: password,
      password_confirmation: password_confirmation,
      account_owner: true
    }
  end
end

So, here is the thing: check the variables names I've associated there are the names of form_records I've defined before. It tries to create an account setting a plan to it and then tries to create a user associated to this brand new account.

This form_records will call each object associated here to check its errors, and group it in errors object in your form itself in case of some validation fails. If all is fine, the form instance is returned to you and you will be able to call methods like persisted?, account, user, valid? and etc...

Are you still there? :D

Let's see this class complete?

class SignupForm < BaseForm
  use_form_records :account, :user

  attribute :email
  attribute :password
  attribute :password_confirmation
  attribute :plan, Plan, default: proc { Plan.default }

  validates :plan, presence: true

  private

  def persist
    @account ||= Account.create plan: plan
    @user ||= account.users.create user_params
  end

  def user_params
    {
      email: email,
      password: password,
      password_confirmation: password_confirmation,
      account_owner: true
    }
  end
end

Hmmm, this looks pretty nice!

I hope this helps someone in the same way it helped me. Thanks!

Contributing

Thank you!

License

The gem is available as open source under the terms of the MIT License.