A few days ago I pushed the next version of Reform: Version 2. While this is still a release candidate, it can be considered stable.
The reason I blog as if it was a major release is: I want you to test, try, and complain. Speak now or forever hold your peace! Now is the time to make me add or change features before we push the final stable 2.0
Here’s why Reform 2 was necessary, and, of course, why it’s awesome.
UPDATE: This is a release note directed to Reform users. If you want to learn more about Reform, read an introduction post.
Too Big!
There’s not a single amazing new feature in Reform 2. This is, if you quickly skim over the changes.
Of course, a lot of things have changed, but more on the inside of Reform.
Reform was getting too big. The form object was doing presentation, deserialization of incoming data, data mapping, coercion, validation, writing to persistence and saving.
For a gem author, monster objects are (or should be!) a nightmare. It is incredibly hard to follow what happens where in big objects, so I extracted a huge chunk of logic into a separate gem.
The form object now really only does validation, everything else is handled via Disposable and Representable.
The Architecture Now.
Both deserialization and mapping form data to persistence objects like ActiveRecord models is now completely decoupled.
To cut it short: Deserializing of the params
hash into a validatable object graph is done by a representer. Validation happens in the form itself. Coercion, syncing and saving all happens in the form’s twin.
Less Representable.
I removed a lot of representable-specific mapping logic, mainly because it was incredibly hard to understand. For example, you can now actually grasp what methods like #prepopulate!
do by looking at the source.
This has also sped up Reform by 50%. That’s right – it is much faster now thanks to explicit, simple transformation logic.
No Rails, More Lotus!
Reform 1 used ActiveModel::Validations
for validations. This still works, but you can also chuck Rails into the bin and use Lotus::Validations
instead – removing any Rails dependency from your forms.
class SongForm < Reform::Form include Reform::Lotus property :title validates :title, presence: true end
While Reform was dragging the activemodel
dependency around, this is now up to you. Reform still supports Rails but with a very low gravity.
Deserialization.
In #validate
, to parse the incoming params
into the object graph, an external representer is used. This could be any kind of representer and thus allows you to parse JSON, XML and other formats easily into an object graph.
Nevertheless, the representer will simply operate on the twin API to populate the form. This means, you can basically use your own deserialization logic.
form = SongForm.new(song) form.title = "Madhouse" form.band = Band.new form.band.name = "Bombshell Rocks" form.validate({})
The above example is a naive implementation of a deserializer without overriding parts of validate
. You can set properties and add or removed nested objects. The twin will take care of mapping that to its object graph.
Forms and JSON
Trailblazer takes advantage of that already and allows JSON “contracts” that can deserialize and validate JSON documents.
You can do that manually, too.
class SongRepresenter < Roar::Decorator include JSON property :title end form.validate('{"title": "Melanie Banks"}') do |json| SongRepresenter.new(form).from_json(json) end
This will use SongRepresenter
for the deserialization. The representer will assign form.title=
. After that, the form will proceed with its normal validation logic as if the form was a hash-based one.
In case I missed to make my point: This allows using forms for document APIs!
Coercion
In earlier versions, Reform implemented coercion in the deserialization representer which sometimes was kinda awkward. Coercion now happens in the twin.
form.created_at = "1/1/1998" form.created_at #=>
You can also override the form’s setter methods to build your own typecasting logic. Many people did that already in Reform 1, but in combination with the representer this could mess things up.
Populators
When deserializing, Reform per default tries to find the matching nested form for you. Often, there is no nested form, yet, that’s why we provide options like :populate_if_empty
that will add a nested form corresponding to the particular input fragment.
Using the :populator
option was a bit tedious and you needed quite some knowledge about how forms work. This has changed in Reform 2 and is super simple now.
In a populator, you can use the twin API to modify the object graph.
populator: lambda do |fragment, collection, index, options| collection << Song.new end
This primitive populator will always add a new song object to the existing collection. Note how you do not have to care about adding a nested form anymore, as you used to have in Reform 1. The twin will do this for you.
Pre-populators
I’ve seen many users writing quirks to “fill out” a form before it is rendered, for example, to provide default values for visual fields or pre-selecting a radio button.
Reform 2 introduces the concept of prepopulators that can be configured per property.
property :title, prepopulator: lambda { self.title = "The title" }
Again, prepopulators can use the twin API to set up an arbitrary object graph state. They have to be run explicitly, usually before rendering, using #prepopulate!
.
Hash Fields
A feature I personally love in Reform 2 is Struct
. It allows to map hashes to properties.
Say you had a serialized hash column in your songs
table.
class Song {admin: {read: true, write: true}, user: {destroy: false}}
“Working with hashes is fun!” said no one ever. Instead, let Reform map that to objects.
class SongForm < Reform::Form property :settings, struct: true do property :admin, struct: true do property :read property :write validates :read, inclusion: [true, false] end end end
You can have an unlimited number of nestings in the hash. Every nesting results in a nested form twin to work with.
The Struct
feature is described in this blog post in greater detail.
Syncing and Saving
The sync
and save
method both completely got extracted and are now implemented in Disposable.
Option Methods
A nice addition that I use a lot is option methods: you can specify dynamic options not only with a lambda, but also as a symbol referencing an instance method.
property :composer, populate_if_empty: :populate_composer! do # .. end def populate_composer!(fragment, options) Artist.new end
This greatly cleans up forms when they become more complex. A cool side-effect: you can use inheritance better, too, and reuse option methods.
State Tracking
Since nested forms are now implemented as twins, you can use Disposable’s state tracking to follow what was going on on your form in validate
.
State tracking is incredibly helpful for Imperative Callbacks and other post-processing logic.
More Documentation!
As you might have noticed, I have started to document all my gems on the new Trailblazer page.
I’d like to point you to the upcoming Trailblazer book, too. In 11 chapters, it discusses every aspect of Reform you can think of, as Reform is an essential part of this new architecture.
As a side-note: I mainly wrote this book to save myself from answering particular questions a hundred times. The Trailblazer books really talks about all my gems in great detail, and it is a nice way to support a decade of Open-Source work for you, too.
Conclusion
With Reform 2.0, my dream architecture has become true, my vision of what a form object should do and what should be abstracted in a separate layer is implemented, and I am very happy with it.
The code should be significantly easier to read and change, too. And it is faster.
It all adds up – Reform 2 is already deployed on hundreds of production sites, so update today and let me know what you think!
When trying to use lotus validations, I get:
NameError: uninitialized constant Reform::Lotus
LikeLike
Seems the module is called: Reform::Form::Lotus
LikeLike
Link to http://trailblazerb.org/ is dead. Is the link correct? Or has been the webiste not launched yet?
LikeLike
Hey Vojta, it should redirect to http://trailblazer.to.
LikeLike