Disciplined Rails: Form Object Techniques & Patterns — Part 3
Part 1: Ground rules for form objects
Part 2: Dealing with collections
Part 3: Tying up loose ends
If you’ve slogged through parts 1 and 2, you have all you need to accomplish pretty much anything with forms. Part 3 is a bit of a grab bag but here’s what we deal with:
- Tackling complex forms with form objects
- Deeper look into validations
- A review of gems in use today to build form objects
Complex forms
So far, we’ve dealt with some pretty meaty uses of the form object when dealing with forms that persist some sort of change to the database, or in our very first example to send out a couple of emails. One of my other favorite uses of the form object is to take over the responsibilities of search forms, and any filtering that it might need to do.
Let’s say we have an admin backend, where we get to manage all the users that are registering on our application, which is now growing very quickly and displaying all the user accounts on your index page is getting rather unwieldy. You decide that you want to be able to search by the person’s name and email, and optionally filter by account status, perhaps what group the account belongs to, etc.
Here’s what it looks like:
class Admin::UserSearchForm
include ActiveModel::Model attr_accessor :term, :status, :group validates :term, length: { minimum: 2 }, allow_blank: true
validates :status, inclusion: { in: %w(active inactive) }, allow_blank: true def options_for_status
@options_for_status ||= [['Active', :active], ['Inactive', :inactive]]
end def options_for_group
@options_for_group ||= Group.all
end def submit
return false if invalid?
@results = Person.all
@results = @results.where('LOWER(name) LIKE LOWER(?)', "#{term}%") if term.present?
@results = @results.where(status: status) if status.present?
@results = @results.where(group: group) if group.present?
@results
end def results
@results || []
endend
Firstly, you’ll see a couple of methods that provide options for the view template, which follows the naming style for the rails options_for_select method. Think of the form object as the interface for the view, and is responsible is providing the right pieces of data for the search form to do its work. As a side note, if you use something aasm, together with rails enums, then you can simply do something like:
def options_for_status
@options_for_status ||= Person.statuses
end
While you could also populate the option values directly in the view template, I prefer to keep it close to the validation concerns as a matter of symmetry, and also it can be the place add logic such as removing options based on what’s selected if need be.
The submit
method is also getting pretty interesting, where you see some use of ActiveRecord method chaining that builds the query over several lines, with its respective conditional checks enforced. Making use of ActiveRecord in this manner reduces the amount of branching that we need to submit this query.
This is all looking pretty good so far (if I say so myself), let’s take a look at the controller here:
def index
@form = Admin::UserSearchForm.new(user_search_form_params)
@form.submit if params[:user_search_form].present?
endprivatedef user_search_form_params
params.fetch(:user_search_form, {}).permit(:term, :status, :group)
end
As straightforward as it gets, only thing to take note is that we only want to call the submit method if there was input parameters provided, otherwise there will be validation errors on the first page load. Ideally, when you first load the search form, it should be empty (your requirements may differ).
Take note of the use of fetch
here. Since we want to allow the form to be empty on first load, we don’t want to trigger an exception right off when trying to retrieve the parameters. Instead, we return an empty hash, which works well with our presence check in the action itself.
Now, for the view:
= simple_form_for @form, url: admin_users_path, method: :get do |f|= f.input :term= f.input :status, collection: @form.options_for_status, include_blank: 'Any status'
= f.input :group, collection: @form.options_for_group, include_blank: 'Any group'= f.submit 'Search'
Nothing too fancy, which is what we want.
So even with a couple of parameters, the submit method is already rather lengthy, with multiple presence checks, and the actual database calls. One improvement you might want to explore here is to build a query object, passing in the form’s parameters, and returning it as the result instead. This requires coordination across the model, view and controller, but can pay off if your form is really complex.
In practice, many form objects straddle between fielding view related concerns, and modifying some sort of state, but more complicated situations warrant a cleaner separation of concerns, such as using a query object. In such a scenario, the form object should be thought of as closer to the view template in HTML, acting solely as the abstraction for the form in the view template itself whose role is in managing user input, and feedback (validation errors), while the query acts as the boundary for retrieving the right data given the form object’s outputs.
Validations deep-dive
One of the questions that come up often in other form object implementations is whether to duplicate validations on the form objects on the model itself, which by doing so violates DRY principles. We’ve effectively side-stepped issue this because most if not all the techniques (dealing with POROs or AR models within a form object) in the series so far depend on ActiveRecord’s nested attributes feature (all objects are nested by default), and so we rarely if ever face that issue since we can place validations in either location (form or model), or both.
The question then is where to put validations. My experience so far has been keeping ActiveRecord models as sparse as possible, and moving logic including validations out has so far proven effective. You want nimble models that you can use in a variety of situations, and unnecessary validations get in the way of that.
I still keep simple validations in the model, especially where it maps to database-level constraints such as whether to allow null for a field. Other times, validations can be highly contextual like having to check a checkbox at registration, or cuts across more than one model, and in those cases, it’s mostly a forgone conclusion to move to the form object.
So while we avoid one type of duplication (between form object and model), we often find ourselves having to duplicate data integrity logic with the database anyway.
Consider the aspect of working with data on as a continuum between two concerns. On one end, we have data integrity concerns, and on the other what user experience concerns. At the data integrity end of things, we want to ensure things like the uniqueness of columns are maintained, so we add the necessary constraints in our DBMS to enforce them. On the other hand, we also add the uniqueness validation in rails because we want to output a useful error message to the user when their input is not unique.
Neither of these validations can stand on their own; you need database-level constraints to guarantee uniqueness, and you want your users to have a decent experience in case of mistakes.
With that in mind, it makes sense to take a layered approach, starting at the database level. What is the minimum number of constraints that you need to ensure a database design that can outlive the application that you are building? Then, at the model level, include only the validations that ensures the integrity of the model in the majority of use cases.
A lot of times, people do too much in the model, including dealing with associated models, and thus end up with convoluted validation logic in one model. A model should care only for its own domain, so move all that out using form objects. If you have cross-cutting concerns that push responsibility to a single model, consider moving that responsibility away into a different model altogether.
To Gem or not to Gem
There has been a number of gems out there help us with building form objects, and if you’ve dutifully followed this series you may scratching your head as to why using them at all. This is especially a concern if we are building memory-efficient rails apps, and want to pare down our Gemfile.
I personally do not use these gems in any of my projects, but my approach in writing this part is to understand the underlying premise of these projects, and evaluating reasons for and against using them.
Reform
Form objects decoupled from your models.
Reform does a pretty good job of living up to its tagline. It covers just about all the techniques we talked about, and I presume that it pretty much hides all the nested attributes magic for you.
One drawback is that the gem also introduces its own lifecyle pipeline (depicted below), which I’m not sure is worth the additional effort of serializing/deserializing its own object graph, which ActiveRecord/ActiveModel already provides us.
In short: use Reform if you don’t use ActiveRecord/ActiveModel, such as if you are not using a relational DBMS.
Yaaf
This gem actually references the first part of this series, and automates some of the techniques used here. It’s only 64 lines of code (I counted), and so I had no excuse but to read all of it.
Primarily, YAAF hooks into the ActiveModel lifecycle to automate calling valid on your downstream objects, and promoting their errors. Admittedly, this has always been an eyesore for me in my own form object implements so this is nice to have but not mission critical.
Despite its simplicity, this gem does come with one caveat, which that it automatically wraps your save calls in a single database transaction. Using transactions is not a bad thing of course, but production code often requires very finely-tuned and nuanced solutions specific to the situation at hand. I prefer if all the database-wrangling code is packed together in a single location, and its full intention made as explicit as possible.
In short: use YAAF if you want cleaner form objects, but not if you have complex transaction needs.
Using a combination of dry-rb gems
If you are familiar with the dry collection, you probably know more than I do when it comes to using it and have some strong opinions on when to use it so I will refrain from straying from the topic at hand. Here’s one way of doing form objects using dry:
From my read on things, it offers less sugar than the above two gems when it comes to wrestling with forms. Where it shines is that you get the full benefits of working with the dry collection of gems, including some very powerful data types for working with data. Again I have no experience using dry-*, but it does look like an awesome arsenal to tap on if you’re into things like functional programming or monads.
In short: use dry-* gems for form objects, if you are already invested in the dry gem ecosystem.
Congratulations on getting through this series hopefully it wasn’t as tiring to read it as it was to write it (dang, this has been 3 years in the making). All apps grow more complex over time, and UI improvements can often be at odds with how we’ve defined our data models; the form object presents a strategic piece in our toolkit in maintaining control in spite of complexity.
With this series, I’ve tried to create the everything-you-need-to-know on form objects; if you think I’ve missed something out or have fallen short in any way, do let me know in the comments.