
RFO - Rails Form Object
Inspired by [7 Patterns to Refactor Fat ActiveRecord Models] (http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/) and SRP
What it does?
- simplify forms with multiple models - we do not have to use nested attributes
- remove validations from models - they do too much already. What is more each form can have different validations.
- can substitude strong parameters - we define exacly what and how we want to assign values in models
Installation
Add this line to your application's Gemfile:
gem 'rfo', github: 'petergebala/rfo'
And then execute:
$ bundle
Or install it yourself as:
$ gem install rfo # Not yet!
Usage
It works great with Draper and Simple Form
Define simple model with relations eventually callbacks:
class Organisation < ActiveRecord::Base
WEBSITE_REGEXP = /[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?/ix
APPLY_FOR = [:under_2000, :over_2000]
belongs_to :grant, touch: true
has_one :address, as: :entity, dependent: :destroy
has_one :contact, as: :entity, dependent: :destroy
has_one :user, through: :grant
after_create :create_address
after_create :create_contact
delegate :postcode, to: :address
end
Define form:
- fields which you will use in form
- validations for this form
- default values
- how and where it should assign values
class OrganisationForm < RFO::Base
attribute :amount_apply_for, String
attribute :organisation_name, String
attribute :charity_number, String
attribute :first_name, String
attribute :last_name, String
attribute :position, String
attribute :address_line_1, String
attribute :address_line_2, String
attribute :address_line_3, String
attribute :town, String
attribute :county, String
attribute :postcode, String
attribute :phone_number, String
attribute :mobile_number, String
attribute :website, String
attribute :description, String
validates :description, presence: true, length: { maximum: 2000 }
validates :amount_apply_for, presence: true, inclusion: { in: Organisation::APPLY_FOR.map(&:to_s) }
validates :organisation_name, presence: true, length: { maximum: 255 }
validates :first_name, presence: true, length: { maximum: 255 }
validates :last_name, presence: true, length: { maximum: 255 }
validates :address_line_1, presence: true, length: { maximum: 255 }
validates :town, presence: true, length: { maximum: 255 }
validates :postcode, presence: true, length: { maximum: 10 }, format: { with: Address::POSTCODE_REGEXP }
validates :phone_number, presence: true, length: { maximum: 20 }, format: { with: Contact::PHONE_NUMBER_REGEXP }
validates :mobile_number, length: { maximum: 20 }, format: { with: Contact::PHONE_NUMBER_REGEXP }, allow_blank: true
validates :website, length: { maximum: 255 }, format: { with: Organisation::WEBSITE_REGEXP }, allow_blank: true
private
def assign_defaults
@grant.current_step = :organisation_details
organisation_contact = @organisation.contact
organisation_address = @organisation.address
user_contact = @organisation.user.contact
user_address = @organisation.user.address
self.organisation_name ||= @organisation.name
self.first_name ||= organisation_contact.first_name || user_contact.first_name
self.last_name ||= organisation_contact.last_name || user_contact.last_name
self.position ||= organisation_contact.position || user_contact.position
self.phone_number ||= organisation_contact.phone_number || user_contact.phone_number
self.mobile_number ||= organisation_contact.mobile_number || user_contact.mobile_number
self.address_line_1 ||= organisation_address.address_line_1 || user_address.address_line_1
self.address_line_2 ||= organisation_address.address_line_2 || user_address.address_line_2
self.address_line_3 ||= organisation_address.address_line_3 || user_address.address_line_3
self.town ||= organisation_address.town || user_address.town
self.county ||= organisation_address.county || user_address.county
self.postcode ||= organisation_address.postcode || user_address.postcode
end
def persist!
ActiveRecord::Base.transaction do |t|
@organisation.name = self.organisation_name
@organisation.amount_apply_for = self.amount_apply_for
@organisation.charity_number = self.charity_number
@organisation.website = self.website
@organisation.description = self.description
@contact = @organisation.contact
@contact.first_name = self.first_name
@contact.last_name = self.last_name
@contact.position = self.position
@contact.phone_number = self.phone_number
@contact.mobile_number = self.mobile_number
@address = @organisation.address
@address.address_line_1 = self.address_line_1
@address.address_line_2 = self.address_line_2
@address.address_line_3 = self.address_line_3
@address.town = self.town
@address.county = self.county
@address.postcode = self.postcode
@grant.current_step = @grant.next_step
@organisation.save!
@contact.save!
@address.save!
@grant.save!
end
end
end
Define skinny controller:
class OrganisationDetailsController < ApplicationController
before_filter :set_grant
respond_to :html
before_filter :set_organisation, only: [:edit, :update]
def edit ; end
def update
flash[:notice] = 'Organisation details saved!' if @organisation_form.update_attributes(organisations_params)
respond_with @organisation_form, location: @grant.current_path
end
private
def set_grant
@grant = current_user.grants.find(params[:grant_id]).decorate
end
def set_organisation
@organisation = @grant.organisation.decorate
@organisation_form = OrganisationForm.new(organisation: @organisation,
grant: @grant)
end
def organisations_params
params.require(:organisation_form).permit(:amount_apply_for,
:organisation_name,
:charity_number,
:first_name,
:last_name,
:position,
:address_line_1,
:address_line_2,
:address_line_3,
:town,
:county,
:postcode,
:phone_number,
:mobile_number,
:website,
:description)
end
end
And clean view!
= simple_form_for @organisation_form, url: grant_organisation_details_path(@grant), method: :patch do |f|
= f.error_notification
h2 About Your Organisation
fieldset
= f.input :amount_apply_for,
collection: @organisation.amount_apply_for_buttons_for_views,
as: :radio_buttons,
wrapper: :bootstrap_group_horizontal
= f.input :organisation_name
= f.input :charity_number
h2 Your Address Details
fieldset
= f.input :first_name
= f.input :last_name
= f.input :position
= f.input :address_line_1
= f.input :address_line_2
= f.input :address_line_3
= f.input :town
= f.input :county
= f.input :postcode
h2 Your Contact Details
fieldset
= f.input :phone_number
= f.input :mobile_number
h2 Your Organisation
fieldset
= f.input :website, as: :addon, input_html: { addon_text: 'http://' }
= f.input :description, as: :text
h2 Proceed
fieldset
= f.button :submit
Additional information
Becasue Form Object is just plain ruby class you can:
- test it in simple way,
- share common code (like validations) between form objects,
- inherit between forms.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
TODO:
- turn off strong_parameters
- write tests
- correct documentation
- update documentation with has_many relation and show how to remove nested_attributes