Formality

Form objects. For rails.

gem install formality

Forms have both data and behavior. Sounds suspiciously like they should be objects. Let's make them so.

Additionally, let's get all that validation logic out of the ORM. Enforcing data integrity is one thing; making sure that you get either a phone number or an email from a user is another.

Quick Example

Let's make a signup form.

class SignupForm
  include Formality

  attribute :email
  attribute :password
  attribute :password_confirmation

  validates_presence_of :email, :password, :password_confirmation
  validates_confirmation_of :password
end

In, say, a UsersController:

class UsersController < ApplicaationController
  def new
    @form = SignupForm.new
  end

  def create
    @form = SignupForm.assign params[:signup_form]

    @form.valid do
      User.create! @form.attributes
    end

    @form.invalid do
      render :new
    end
  end
end

In the view:

= form_for @form do |f|
  = f.text_field :email, :placeholder => "Email"
  = f.password_field :password, :placeholder => "Password"
  = f.password_field :password_confirmation, :placeholder => "Confirm Password"
  = f.submit "Sign up!"

More docs

Attributes

Use the attribute class method to declare attributes. It accepts a :default option.

class TodoForm
  include Formality

  attribute :description
  attribute :done, :default => false
end

default = TodoForm.new
default.description    #=> nil
default.done           #=> false

assign is available as a class or instance method, and will:

  1. Assign any declared attributes from the hash it receives.
  2. Assign an :id if one is present, whether declared or not (this allows us to get some nice behavior when working with models).
  3. Return the form object.
# Continuing using the TodoForm

form = TodoForm.assign :id => 1, :description => "Buy some milk"
# or
form = TodoForm.new.assign :id => 1, :description => "Buy some milk"

form.id          #=> 1
form.description #=> "Buy some milk"
form.done        #=> false

attributes gets you a Hash of the attributes:

form = TodoForm.assign :id => 1, :description => "Buy some milk"

form.attributes #=> {"description" => "Buy some milk", "done" => false}

# It's an ActiveSupport::HashWithIndifferentAccess, so:
attrs = form.attributes
attrs[:description] == attrs["description"] == "Buy some milk"

attribute_names returns an Array of attribute names:

form = TodoForm.new
form.attribute_names #=> ["description", "done"]

Validations

This one's easy to write docs for, as they are [already written][validddocs].

You get all your standard ActiveModel validations to play with:

class SignupForm
  include Formality

  attribute :email
  attribute :password
  attribute :password_confirmation

  validates_presence_of :email, :password, :password_confirmation,
    :message => "can't be blank"
  validates_confirmation_of :password,
    :message => "confirmation mismatch"
end

form = SignupForm.assign :password => "123", :password_confirmation => "456"
form.valid?            #=> false
form.invalid?          #=> true
form.errors[:email]    #=> ["can't be blank"]
form.errors[:password] #=> ["confirmation mismatch"]

You can, of course, use :valid? and :invalid? directly, but Formality also provides with a little bit of sugar:

form = SignupForm.assign(some_attrs_from_somewhere)

form.valid do
  # this block will run only if the form is valid
end

form.invalid do
  # this block will run only if the form is NOT valid
end

It's a couple of lines longer than if form.valid?...else...end, but I feel it reads a little more clearly. Use it if you like it, don't if you don't.

Working With Models

The from_model class method builds a from object from an associated model. The model object must have an attributes method (in case you're looking at using this with something that's not ActiveRecord).

The model class method lets you specify which model the form object represents. This lets form_for generate the right resourceful routes for your object from the form object itself.

# config.routes.rb
resources :todos

# models/todo.rb
class Todo < ActiveRecord::Base; end

# models/forms/todo_form.rb (or wherever you want to put it)
class TodoForm
  include Formality

  model :todo

  attribute :description
  attribute :done, :default => false
end

todo = Todo.create!(:description => "Feed the dog")
todo.id          #=> 42 (or whatever)

form = TodoForm.from_model(todo)
form.id          #=> 42
form.description #=> "Feed the dog"
form.done        #=> false

# Now, when you pass your form into `form_for`, it will
# automatically create a form that points to "/todos/42"
# with the method set to put.

Nesting

Use nest_many or nest_one to declare nested form objects. This will define reader and writer methods such that the form object will work cleanly with a fields_for call.

If you also add a :from_model_attribute option, nested form objects will get correctly populated when you use from_model.

Question = Struct.new(:id, :text, :answers) do
  def attributes; Hash[members.zip(values)] end
end

Answer = Struct.new(:id, :text) do
  def attributes; Hash[members.zip(values)] end
end

class QuestionForm
  include Formality
  attribute :text
  nest_many :answer_forms, :from_model_attribute => :answers
  validates_presence_of :text, :message => "can't be empty"
end

class AnswerForm
  include Formality
  attribute :text
  validates_presence_of :text, :message => "can't be empty"
end

question = Question.new(1, "Question 1")
question.answers = [Answer.new(1, "Answer 1"), Answer.new(2, "Answer 2")]

form = QuestionForm.from_model(question)
form.attributes #=> {"text" => "Question 1",
                     "answer_forms_attributes" => [{"text"=>"Answer 1"},
                                                   {"text"=>"Answer 2"}]}

form.answer_forms[0].id #=> 1
form.answer_forms[1].id #=> 2

I think there are still maybe some API kinks to work out with the nested forms, but this gets it most of the way there.