BaseForm

Code Climate Test Coverage Codeship Status for andrerpbts/base_form

A simple and small Form Objects Rails plugin for ActiveRecord based projects.

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

In a development day-to-day basis, we commonly are confronted with situation 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.

That said, let me show some simple examples to you. 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? Did not should be validated as well? Well, you could, in fact, perform all validations in this form, but sometimes this can happen. 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 the talk is over. Otherwise, it wil try to persist your logic, that 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 put 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 this 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, 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 erros 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 ||= .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, see the variables names I've associated there, are the name os 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.

Is this form_records that 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 brakes, but 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 ||= .users.create user_params
  end

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

Hmmmm, seems to be good enough. I hope this helps someone in the same way it helped me. Thanks!

Contributing

  • Fork it
  • Make your implementations
  • Send me a pull request

Thank you!

License

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