Feb 10, 2016
Table of contents:
Form Objects have become a very well recognised pattern in the world of Ruby on Rails.
We have previously looked at creating our own Form Objects in using Active Model and Virtus (Using Form Objects in Ruby on Rails.
A lot of Rails developers like to write their own Form Object class that can be used as a base for all of the forms in the application.
But, with it being such a well recognised pattern, there are existing Open Source projects that look to handle this problem for us.
Open Source libraries tend to be better designed than hand-rolled solutions as they usually cover a lot of edge cases that you won’t think about at the start of your project.
Using an Open Source library will also save you a lot of headache if you are a newbie developer and you’re just trying to get the damn thing to work!
One good Open Source option is Reform, and so, in today’s tutorial we will look at creating Form Objects using Reform.
As a quick recap we will look at the responsibility of the Form Object and why we need this extra layer during the request.
Firstly, A common convention of Ruby on Rails is to have the validation inside the model.
However, a typical web application will require certain specific validation rules under particular circumstances.
For example, when the user first signs up, you might want them to type their chosen password in twice for confirmation.
However, checking that a password has been typed correctly has nothing to do with the business rules of the User
model and will only be required when the user first signs up to the application.
Secondly, you will often want to create multiple models at the same time during a request.
For example, when a user signs up, you might also need to create an Account
model object too.
If you try and force one model to be responsible for creating another model, you will end up with a nested mess of code.
A Form Object sits in the middle of this problem to coordinate the validation of form specific data and the creation of the required models for the request.
This encapsulation provides a nice and easy way to capture the business logic and responsibility into one single object that can be used in the controller and the view.
So with that little explanation out of the way, lets to start creating Form Objects!
As I mentioned in the introduction to this post, I’m going to be using Reform for this project.
So the first thing we need to do is to add it to the project:
gem 'reform'
gem 'reform-rails'
Next, run the following command from Terminal:
bundle install
We’re going to need a directory to store all of the forms for the application. Create a new forms
directory under app
and another one under test
.
So now that we have Reform added to the project we can start adding the first form.
However, before I actually create the form, first I will sketch out the requirements as tests. The first form I will be making will be for creating a new article.
Create a new file called article_form_test.rb
under the test/forms
directory:
require 'test_helper'
class ArticleFormTest < ActiveSupport::TestCase
def setup
@model = Article.new
@form = ArticleForm.new(@model)
end
end
The first thing I’m going to do is to create the ArticleForm
in a setup
method so I don’t have to repeat myself before each test.
Next I’m going to write a couple of validation-type tests for the properties of the form. If I were doing strict TDD I would go through the motions for each individual test, but to be honest, I think the ceremony of TDD is a bit of a waste when you are doing simple stuff like this.
test 'should require title' do
@form.validate({})
assert_includes(@form.errors[:title], "can't be blank")
end
test 'title should be unique' do
@form.validate('title' => 'Hello World')
assert_includes(@form.errors[:title], 'has already been taken')
end
test 'should require markdown' do
@form.validate({})
assert_includes(@form.errors[:markdown], "can't be blank")
end
test 'should require published_at' do
@form.validate({})
assert_includes(@form.errors[:published_at], "can't be blank")
end
test 'should require user' do
@form.validate({})
assert_includes(@form.errors[:user], "can't be blank")
end
If you run these tests you should see them all fail. With failing tests in place we can start to build the form.
The first thing we need to do is to add a new file called article_form.rb
under the app/forms
directory:
class ArticleForm < Reform::Form
end
As you can see from this example, your Form Objects should inherit from Reform::Form
.
Next we need to define the properties of the form:
class ArticleForm < Reform::Form
property :title
property :markdown
property :published_at
property :user
end
The Form Object is responsible for validation and so we can define some validation rules:
validates :title, presence: true, unique: true
validates :markdown, presence: true
validates :published_at, presence: true
validates :user, presence: true
Reform recommends that you use it’s non-writing uniqueness validation. To do that we need to add the following line to the top of this file:
require 'reform/form/validation/unique_validator.rb'
Now if you run those tests from earlier again, you should see them all pass.
A couple of weeks ago I looked at converting Markdown to HTML (Rendering Markdown and HTML in Ruby). In this new version of Culttt I want to write my articles in Markdown and then have the application automatically convert them to HTML on save.
Reform makes it really easy to define your own custom save behaviour by simply implementing the save
method on the form:
def save
sync
end
The first thing we need to do is call the sync
method from the parent class. This will automatically call the accessor methods on the model with each of the properties of the form.
Next we can define our custom behaviour, and then call save
on the model.
First I want to automatically generate the slug of the article from the title:
model.slug = title.to_url
To convert a string to a slug, I’m using the Stringex gem that adds some useful extensions to Ruby’s String
class.
Next I want to generate the HTML for the article from the given Markdown:
model.html = Render::HTML.new.render(markdown)
And finally I will call save!
on the model
:
model.save!
We can test that this is working correctly with the following test:
test 'should create the article' do
@form.validate(
title: 'My first blog post',
markdown: '# My first blog post',
published_at: Time.now.to_s,
user: User.first
)
assert(@form.save)
end
The example we have looked at so far has been fairly simple. However, in the real world, things are never that simple.
A common requirement when dealing with forms is that multiple models are created at the same time. This can make things a lot more complicated.
Fortunately, Reform has been written in such a way that it makes dealing with nested models really easy.
For example, lets rewrite the test from above like this:
test 'should create the article' do
@form.validate(
title: 'My first blog post',
markdown: '# My first blog post',
published_at: Time.now.to_s,
user: User.first,
tags: [{ name: 'Code' }]
)
assert(@form.save)
assert_equal(1, @model.tags.count)
end
In this version of the test I’ve added a collection of tags that should be added to the article. In my database, each Article
is related to one or more Tag
objects through a has has_many :through
relationship.
So now we also need to deal with finding or creating each Tag
on save and then associating them with the new Article
.
The first thing we need to change is to add a collection
to the Form Object:
collection :tags, populate_if_empty: :populate_tags! do
property :name
property :slug
end
Here I’m defining the tags
collection and I’m telling Reform to call the populate_tags!
method if the collection is empty.
Next we can define that method:
def populate_tags!(options)
Tag.find_by(name: options[:fragment][:name]) or
Tag.new(slug: options[:fragment][:name].to_url)
end
In the populate_tags!
method we can attempt to find an existing tag with the given name and return it, otherwise we can create a new tag.
If you run those tests again, you should see them all pass!
Today’s tutorial has been a fairly simple example of dealing with the requirements of a Form Object.
I often find with this type of functionality, you roll your own solution because things are simple in the beginning. But over time, your hand-rolled solution starts to get more and more complicated as the requirements of additional forms drift from your early forms.
That is why I usually try to use an Open Source gem like Reform.
Dealing with the implementation details of how the Form Objects work in my application will have very little impact on the success of my application.
So instead of wasting time writing my own solution, I think my time is better spent worrying about the business logic of my application, and letting the wonderful world of Open Source worry about the edge cases I will no doubt encounter as my application continues to grow!