Form Object is a very useful pattern in rails ecosystem as it can address variety of issues and antipatterns. In this article we will attempt to sort out in which situations Form Objects can prove useful and what conventions allow us to make most out of them.
Table of Contents
When to use Form Objects?
Replacement for Strong Parameters
I am not a huge fan of strong parameters in rails and therefore I do enjoy what Form Object brings to the table. As each Form Object has all of its attributes explicitly defined, we can be sure, that no extra attributes will be persisted in case of parameter tampering.
What is more, we have full freedom of adding any kind of attributes using any technique (i.e. using Virtus), so we can take advantage of attributes coercion, default values or virtual attributes with little to no effort.
A good place for model lifecycle callbacks
We should always be cautious when adding callbacks to our models, due to the variety of reasons. If we are determined to move the callback logic outside of a model, we obviously need to decide, where to move this logic to. This is where Form Objects can usually help. While callbacks related to destroying a record will fit better in service objects, those related to creation and validation of record can be easily placed in persistence logic of a Form Object.
Form Object as virtual resource
Form Object does not necessarily need to map 1 to 1 with a model. Not only can we use a different set of attributes but we may also decide not to represent any model at all. A good example would be a contact form — if we decide not to store a message sent in database, our persistence logic can focus only on scheduling email delivery, while the Form Object will help by facilitating rendering of the actual form in UI.
Form Object as composed resource
If you need one form to save data into multiple models, then you have quite a few techniques available. Rails Nested Attributes is one of them, but it requires us to carefully construct attribute names. This usually does not play nice with forms generated by frontend frameworks without burdensome configuration. We can handle all the logic in a controller, but it just does not feel right to increase its complexity. Service objects may help, still usually it might feel awkward to create a service responsible solely for persisting data.
Form Objects are a good fit here. Not only are we not limited to any specific attribute names but we can also fully and explicitly control the logic of creating data in multiple tables. Finally, forms are designed for saving stuff, so using them just feels right.
Form Object as options provider
Sooner or later our forms, as seen from UX perspective, will need some dropdowns representing options available. Be it the list of groups we want to associate the user with. The usual question is where to put logic responsible for generating this list of options. One way to go is to include it directly in template — it is not recommended to include any complex logic within view layer though, especially if data retrieved bypasses a controller. Using helpers is another obvious choice, but exposing something very specific this way just does not seem right — what is more, we cannot influence the logic of such helper without passing some extra arguments into it.
Form Objects seem to be a perfect solution here. We not only have a dedicated place for this kind of code, that is easily testable and we can affect the logic using form’s internal state, but also it just feels right to have code so closely correlated with the form in the form itself.
How to make most out of Form Object pattern?
1 Stick to one naming convention
Rule applicable to almost any pattern. In case of Form Objects it feels reasonable to suffix such classes with
Form, to help differentiate forms from models. If a form maps to a single model, including this model’s name in form name also seems to be a good idea. In other cases we should go after making the name as expressive as possible.
UserSettingsForm sound like good examples of such expressiveness.
2 Implement #save and #save!
It appears to be a good idea not to differentiate form’s API to much from model’s API. This will make using forms natural and easy to work with. Therefore, to trigger form’s persistence logic we should use
save method or its exception-raising brother
3 Provide validations capability
As a follow-up to previous point, it might be beneficial to include ActiveModel::Validations in our form objects to enable validation capabilities on all attributes. It is worth mentioning that not all validations will work out of the box — a good example here is uniqueness validation that depends heavily on model’s API and needs to be handled in a custom way in form objects (at least without providing complex logic that would enable such validation to work with forms properly).
4 Implement #param_key
In order to facilitate ease of use of forms any further, we should provide implementation of
param_key and potentially
model_name methods. This will allow us to pass form objects directly as
form_for helper argument without any extra logic. Most of ready-to-use abstractions of form object have this capability built-in.
5 Keep persistence logic in one place
Similarly to service object’s
call method, it is a good convention to keep the most important responsibility of our form within one, pre-defined method.
persist (being called by
save) sounds to be a good name for it, but anything expressive and self-explanatory would work as well.
As stated before, Form Object can help in mitigating a handful of various problems when implementing Rails projects and therefore with little effort it can prove to be a useful pattern in our toolbox. If you feel you might need some simple abstraction around form object pattern, consider trying the reform gem or an even thinner layer provided by the rails-patterns gem.