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:
- Assign any declared attributes from the hash it receives.
- Assign an
:id
if one is present, whether declared or not (this allows us to get some nice behavior when working with models). - 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.