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
default.done
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.
form = TodoForm.assign :id => 1, :description => "Buy some milk"
form = TodoForm.new.assign :id => 1, :description => "Buy some milk"
form.id
form.description
form.done
attributes
gets you a Hash of the attributes:
form = TodoForm.assign :id => 1, :description => "Buy some milk"
form.attributes
attrs = form.attributes
attrs[:description] == attrs["description"] == "Buy some milk"
attribute_names
returns an Array of attribute names:
form = TodoForm.new
form.attribute_names
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?
form.invalid?
form.errors[:email]
form.errors[:password]
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
end
form.invalid do
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.
resources :todos
class Todo < ActiveRecord::Base; end
class TodoForm
include Formality
model :todo
attribute :description
attribute :done, :default => false
end
todo = Todo.create!(:description => "Feed the dog")
todo.id
form = TodoForm.from_model(todo)
form.id
form.description
form.done
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
"answer_forms_attributes" => [{"text"=>"Answer 1"},
{"text"=>"Answer 2"}]}
form.answer_forms[0].id
form.answer_forms[1].id
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.