Decouple your models from forms. Reform gives you a form object with validations and nested setup of models. It is completely framework-agnostic and doesn't care about your database.
Although reform can be used in any Ruby framework, it comes with Rails support, works with simple_form and other form gems, allows nesting forms to implement has_one and has_many relationships, can compose a form from multiple objects and gives you coercion.
Add this line to your Gemfile:
gem 'reform'
You're working at a famous record label and your job is archiving all the songs, albums and artists. You start with a form to populate your songs
table.
class SongForm < Reform::Form
property :title
property :length
validates :title, presence: true
validates :length, numericality: true
end
To add fields to the form use the ::property
method. Also, validations no longer go into the model but sit in the form.
In your controller you'd create a form instance and pass in the models you wanna work on.
class SongsController
def new
@form = SongForm.new(Song.new)
end
You can also setup the form for editing existing items.
class SongsController
def edit
@form = SongForm.new(Song.find(1))
end
Your @form
is now ready to be rendered, either do it yourself or use something like Rails' #form_for
, simple_form
or formtastic
.
= form_for @form do |f|
= f.input :name
= f.input :title
After a form submission, you wanna validate the input.
class SongsController
def create
@form = SongForm.new(Song.new)
#=> params: {song: {title: "Rio", length: "366"}}
if @form.validate(params[:song])
Reform uses the validations you provided in the form - and nothing else.
We provide a bullet-proof way to save your form data: by letting you do it!
if @form.validate(params[:song])
@form.save do |data, nested|
data.title #=> "Rio"
data.length #=> "366"
nested #=> {title: "Rio"}
Song.create(nested)
end
While data
gives you an object exposing the form property readers, nested
is a hash reflecting the nesting structure of your form. Note how you can use arbitrary code to create/update models - in this example, we used Song::create
.
To push the incoming data to the models directly, call #save
without the block.
@form.save #=> populates song with incoming data
# by calling @form.song.title= and @form.song.length=.
Think of @form.save
as a sync operation where the submitted data is written to your models using public setters.
Note that this does not call save
on your models per default: this only happens in an ActiveRecord environment or when Form::ActiveRecord
is mixed in (learn more here).
Songs have artists to compose them. Let's say your Song
model would implement that as follows.
class Song < ActiveRecord::Base
has_one :artist
end
The edit form should allow changing data for artist and song.
class SongForm < Reform::Form
property :title
property :length
property :artist do
property :name
validates :name, presence: true
end
#validates :title, ...
end
See how simple nesting forms is? By passing a block to ::property
you can define another form nested into your main form.
This setup's only requirement is having a working Song#artist
reader.
class SongsController
def edit
song = Song.find(1)
song.artist #=> <0x999#Artist title="Duran Duran">
@form = SongForm.new(song)
end
When rendering this form you could use the form's accessors manually.
= text_field :title, @form.title
= text_field "artist[name]", @form.artist.name
Or use something like #fields_for
in a Rails environment.
= form_for @form |f|
= f.text_field :title
= f.text_field :length
= f.fields_for :artist do |a|
= a.text_field :name
The block form of #save
would give you the following data.
@form.save do |data, nested|
data.title #=> "Hungry Like The Wolf"
data.artist.name #=> "Duran Duran"
nested #=> {title: "Hungry Like The Wolf",
# artist: {name: "Duran Duran"}}
end
Supposed you use reform's automatic save without a block, the following assignments would be made.
form.song.title = "Hungry Like The Wolf"
form.song.artist.name = "Duran Duran"
Reform also gives you nested collections.
Let's have Albums with songs!
class Album < ActiveRecord::Base
has_many :songs
end
The form might look like this.
class AlbumForm < Reform::Form
property :title
collection :songs do
property :title
validates :title, presence: true
end
end
This basically works like a nested property
that iterates over a collection of songs.
Reform will expose the collection using the #songs
method.
= text_field :title, @form.title
= text_field "songs[0][title]", @form.songs[0].title
However, #fields_for
works just fine, again.
= form_for @form |f|
= f.text_field :title
= f.fields_for :songs do |s|
= s.text_field :title
The block form of #save
will expose the data structures already discussed.
@form.save do |data, nested|
data.title #=> "Rio"
data.songs.first.title #=> "Hungry Like The Wolf"
nested #=> {title: "Rio"
# songs: [{title: "Hungry Like The Wolf"},
# {title: "Last Chance On The Stairways"}]
end
Sometimes you might want to embrace two (or more) unrelated objects with a single form. While you could write a simple delegating composition yourself, reform comes with it built-in.
Say we were to edit a song and the label data the record was released from. Internally, this would imply working on the songs
table and the labels
table.
class SongWithLabelForm < Reform::Form
include Composition
property :title, on: :song
property :city, on: :label
model :song # only needed in ActiveModel context.
validates :title, :city, presence: true
end
Note that reform needs to know about the owner objects of properties. You can do so by using the on:
option.
Also, the form needs to have a main object configured. This is where ActiveModel-methods like #persisted?
or '#id' are delegated to. Use ::model
to define the main object.
The constructor slightly differs.
@form = SongWithLabelForm.new(song: Song.new, label: Label.new)
After you configured your composition in the form, reform hides the fact that you're actually showing two different objects.
= form_for @form do |f|
Song: = f.input :title
Label in: = f.input :city
When using `#save' without a block reform will use writer methods on the different objects to push validated data to the properties.
Here's how the block parameters look like.
@form.save do |data, nested|
data.title #=> "Rio"
data.city #=> "London"
nested #=> {
# song: {title: "Rio"}
# label: {city: "London"}
# }
end
Often you want incoming form data to be converted to a type, like timestamps. Reform uses virtus for coercion, the DSL is seamlessly integrated into Reform with the :type
option.
Be sure to add virtus
to your Gemfile.
require 'reform/form/coercion'
class SongForm < Reform::Form
include Coercion
property :written_at, type: DateTime
end
@form.save do |data, nested|
data.written_at #=> <DateTime XXX>
Virtual fields come in handy when there's no direct mapping to a model attribute or when you plan on displaying but not processing a value.
Often, fields like password_confirmation
shouldn't be retrieved from the model. Reform comes with the :empty
option for that.
class PasswordForm < Reform::Form
property :password
property :password_confirmation, :empty => true
Here, the model won't be queried for a password_confirmation
field when creating and rendering the form. Likewise, when saving the form, the input value is not written to the decorated model. It is only readable in validations and when saving the form.
form.validate("password" => "123", "password_confirmation" => "321")
form.password_confirmation #=> "321"
The nested hash in the block-#save
provides the same value.
form.save do |f, nested|
nested[:password_confirmation] #=> "321"
Almost identical, the :virtual
option makes fields read-only. Say you wanna show a value, but not process it after submission, this option is your friend.
class ProfileForm < Reform::Form
property :country, :virtual => true
This time reform will query the model for the value by calling model.country
.
You want to use this to display an initial value or to further process this field with JavaScript. However, after submission, the field is no longer considered: it won't be written to the model when saving.
It is still readable in the nested hash and through the form itself.
form.save do |f, nested|
nested[:country] #=> "Australia"
f.country #=> "Australia"
Reform doesn't really know whether it's working with a PORO, an ActiveRecord
instance or a Sequel
row.
When rendering the form, reform calls readers on the decorated model to retrieve the field data (Song#title
, Song#length
).
When saving a submitted form, the same happens using writers. Reform simply calls Song#title=(value)
. No knowledge is required about the underlying database layer.
Nesting forms only requires readers for the nested properties as Album#songs
.
Check out @gogogarret's sample Rails app using Reform.
Rails and Reform work together out-of-the-box.
However, you should know about two things.
- In case you explicitely don't want to have automatic support for
ActiveRecord
and form builder:require reform/form
, only. - In some setups around Rails 4 the
Form::ActiveRecord
module is not loaded properly, usually triggering aNoMethodError
sayingundefined method 'model'
. If that happened to you,require 'reform/rails'
manually at the bottom of yourconfig/application.rb
.
Reform provides the following ActiveRecord
specific features. They're mixed in automatically in a Rails/AR setup.
- Uniqueness validations. Use
validates_uniqueness_of
in your form. - Calling
Form#save
will explicitely callsave
on your model (added in 0.2.1) which will usually trigger a database insertion or update.
As mentioned in the Rails Integration section some Rails 4 setups do not properly load.
You may want to include the module manually then.
class SongForm < Reform::Form
include Reform::Form::ActiveRecord
Forms in Reform can easily be made ActiveModel-compliant.
Note that this step is not necessary in a Rails environment.
class SongForm < Reform::Form
include Reform::Form::ActiveModel
end
If you're not happy with the model_name
result, configure it manually.
class CoverSongForm < Reform::Form
include Reform::Form::ActiveModel
model :song
end
This is especially helpful when your framework tries to render cover_song_path
although you wanna go with song_path
.
To make your forms work with all the form gems like simple_form
or Rails form_for
you need to include another module.
Again, this step is implicit in Rails and you don't need to do it manually.
class SongForm < Reform::Form
include Reform::Form::ActiveModel
include Reform::Form::ActiveModel::FormBuilderMethods
end
Composed multi-parameter dates as created by the Rails date helper are processed automatically. As soon as Reform detects an incoming release_date(i1)
or the like it is gonna be converted into a date.
Note that the date will be nil
when one of the components (year/month/day) is missing.
By explicitely defining the form layout using ::property
there is no more need for protecting from unwanted input. strong_parameter
or attr_accessible
become obsolete. Reform will simply ignore undefined incoming parameters.
When nesting form, you usually use a so-called inline form doing property :song do .. end
.
Sometimes you wanna specify an explicit form rather than using an inline form. Use the form:
option here.
property :song, form: SongForm`
The nested SongForm
is a stand-alone form class you have to provide.
When "real" coercion is too much and you simply want to convert incoming data yourself, override the setter.
class SongForm < Reform::Form
property :title
def title=(v)
super(v.upcase)
end
This will capitalize the title after calling form.validate
but before validation happens. Note that you can use super
to call the original setter.
If you run into any trouble chat with us on irc.freenode.org#trailblazer.
Great thanks to Blake Education for giving us the freedom and time to develop this project while working on their project.