00:00:13.219
I work at an agency called Industrial, based out of Ottawa. I might be the only Ottawa developer not working for Shopify at RailsConf, but we're an agency that works closely with clients. The software we develop is constantly evolving as we work on it.
00:00:16.789
This is a somewhat sanitized way of saying that clients change their minds a lot, which is totally fine. However, the challenge lies in finding ways to design software that is open and flexible to change. I recently finished a project that involved a significant survey, which comprised many forms. The entire application was essentially just one large form.
00:00:39.719
Before I started, I wanted to devise a solution that would be adaptable, as we knew that the survey was not set in stone. There would be new questions to add and old questions to remove. Additionally, the path the user would follow from one form to the next would constantly shift.
00:01:00.059
This presented a challenge. When your data model aligns with your form, it becomes an easy problem to solve. Rails has excellent defaults for creating a form that matches your data model.
00:01:13.500
For instance, if you need to nest models, you would use 'accepts_nested_attributes_for'. This can feel somewhat magical, and I’m not entirely sure how it's happening, but once your forms become a bit more complex, you end up resorting to hacky if statements in your models for validations.
00:01:30.990
This can quickly become unruly and difficult to understand and manage. Since I've been curious about finding better ways to tackle this problem for a long time, I've discovered countless approaches, and much of it depends on your specific use case and requirements.
00:01:47.100
For example, one approach is to store fields in the session, which works well if you have a wizard where a user fills in information such as their name, email, and address, storing all of this data in the session until the last step when everything is saved into the database.
00:02:01.560
Another option is a gem called Wicked, which simplifies multi-step forms into manageable steps while allowing you to add conditional validations based on which step the user is on. State machines are another option I’ve tried; however, I find they often start simply but can quickly become unruly. It's much like using a flamethrower to light a candle.
00:02:19.480
Alternatively, you can nest your models. I once attended an insightful talk by Andy Malay at RailsConf, which introduced the idea of breaking a large model into smaller pieces and validating each part separately. While these options are interesting, they may not necessarily suit my needs.
00:02:45.510
The real issue with many of these approaches is that models are often forced to conform to presentation details; they try to align the form with the database to ease validations. However, I firmly believe that models shouldn't be concerned with presentation details.
00:03:11.500
Forms exist in a unique space; they are part view and part model simultaneously. While we all understand not to mix business logic into our views, it's seldom discussed that we should avoid introducing view logic into our models. This is Ruby, after all, and we can do whatever we wish.
00:03:41.709
Given my reservations about the options I've mentioned, I found a solution that resonates with me: form objects. Form objects are a great alternative because they are model-agnostic, meaning you aren't required to bunch all your data into a single large model. Instead, you can create smaller models focused solely on gathering the data needed for the form.
00:04:06.729
These form objects act as a custom layer between your view and your model. In this presentation, I will take you through a few ways of creating these forms—both by rolling your own and by utilizing a gem called Reform.
00:04:29.150
First, let’s clearly define what a form object is. Simply put, a form object is just an object that we pass into Form for creation. This is a standard way of creating a form by passing an Active Record model into the form. However, a form object is not limited to just Active Record models. You can use any objects you require. This is one of the true powers of form objects.
00:04:50.040
Instead of being confined to Active Record models, we can compose objects from any attributes present within our database. This flexibility unlocks the potential for complex and adaptable designs.
00:05:09.450
Now, let's look at an example to clarify this concept. Imagine we are creating a service where dog-walking companies can sign up and manage their clients. This process begins with the onboarding wizard, which consists of three forms.
00:05:35.590
First, we ask the user for the name and phone number of their company. The second form allows them to input several addresses, where they can easily add or remove multiple entries. Finally, the settings panel captures information such as the company's name, size, subscription type, time zone, and language preference.
00:05:55.220
Despite appearing straightforward, there are challenges we face. The first challenge in the first form deals with nested data. Here, the phone model is a child of the company model. This raises a question: is there a better alternative to using 'accepts_nested_attributes_for'?
00:06:18.080
In the second form, our data aligns quite closely with our model. This leads me to ponder whether utilizing a form object in this instance is a good alternative, and what advantages it would present.
00:06:43.300
In the third form, data scattering is apparent, as we're saving information back to the company model again. Moreover, all these fields are required, and we aim to save each step of the process as we progress through the forms.
00:07:05.330
This can complicate validation, especially if we need to validate the entire model upon saving. Outside of this simple example, there could be many more attributes within these models, each with their own validation rules and potentially necessitating their own forms.
00:07:35.450
Let’s now build the first form using a form object with the tools provided by Rails, after which I'll refactor the code to use the Reform gem, and we can compare both approaches.
00:07:50.930
This is the standard Rails controller; there’s nothing particularly exciting about it. The only distinction is that our instance variable set for our view is a 'company form'. For the 'create' action, we are passing in some company parameters, using strong parameters for attributes like name and number.
00:08:07.080
Even though the phone number is nested, we don't have to remember the syntax for strong parameters in this case. The same goes for our form view; I’m using Simple Form here, but Form for works well too. You'll notice that we’re passing our company form to pull in the company's name and phone number.
00:08:34.780
Also, take note that these form inputs are flat, with no nesting occurring at all; there isn’t any need for form fields block, even though the phones are nested under the company. Next, we create our form object; we simply need to define a new class and include Active Model.
00:09:04.750
Active Model will provide us with validation, translations, and allow us to create an object that closely resembles an Active Record model. We can define attributes such as name and number and specify where those attributes are to be saved once they’re submitted.
00:09:28.450
For instance, in this case, the name would go to a new company record, while the phone number would be nested under the company. We then implement the validations, beginning with your standard presence validation. You can get fancy if you like, using regex for the phone number, but for simplicity's sake, we’ll keep it basic for this presentation.
00:09:54.660
Next, we need to point to a method must display validation errors. Rails typically uses 'accepts_nested_attributes_for' to display nested error messages. If we don’t define this method, nested models, such as the phone model, would fail validation, but the error messages wouldn’t appear since they are nested under the company.
00:10:19.990
Thus, we write a method, ‘display_errors’, which will transfer error messages from child relationships up to the parent. Our company model in this scenario will act like a parent, pointing to the company controller, just as the company model would.
00:10:43.660
Now, we need to define our own 'save' method. When we call 'save', the validations run. If the form is invalid, the error messages will bubble up. If it’s valid, we’ll start a database transaction that calls 'save' on everything. These are bang methods, meaning if anything fails within, no change will occur.
00:11:06.820
The controller cannot infer the ID of the company during this process and we need to create that ID method in our form object using 'delegate'. This concludes our custom form object, which is straightforward to write and maintains consistency with how we typically work in Rails.
00:11:25.520
However, the drawback of developing your own form object is that you have to build every component from scratch—from making it save, to bubbling up validations. The more complicated your form becomes, the more complex the logic behind the foundational aspects you’ll need to implement.
00:11:47.060
This is where the Reform gem comes into play, which is part of the Trailblazer ecosystem. Trailblazer offers a distinct way to extend the MVC concept. Rather than organizing your code strictly by Model, View, and Controller, you group it by concepts, where each concept can contain service objects called operations, and Reform serves as one of those operations, specifically for building forms.
00:12:20.140
One remarkable aspect of Trailblazer is that you can use specific features without having to integrate the entire framework into your application. In the last application I built, I only used the Reform gem, and it worked magnificently.
00:12:45.580
Now, let’s refactor the form object that we just created to leverage the Reform gem. Assuming we've installed the gem, our form object will inherit from 'Reform::Form'. Reform does not know about Rails by default, so we omit including 'ActiveModel'.
00:13:08.450
Similar to our accessor methods, we will now use 'properties' in our new form object, serving the same purpose: defining and writing to our attributes. Previously, we had to define the model our object communicates with. One difference is that the controller now passes the argument when calling our form object.
00:13:37.300
Thus, we go about it by passing in our company instead of passing a new instance of the form object. We can also eliminate the delegate method since we're passing in the company entity directly.
00:14:01.820
When I mention validation, I'd like to clarify that much of the custom logic we wrote for managing error messages bubbling up from nested models is no longer relevant, as Reform covers this for us. The method referring to our new company is now defined for us and can infer what the new company is.
00:14:26.700
Instead of being present in the form object, it has transitioned to the controller. When saving nested fields, we need to define the nested fill method. Since it’s a nested object under the company form, we need to define it inside the form itself.
00:14:56.440
Reform has a specific way to define relationships of this type that it terms a 'collection', which can be specified using a block with the same name as the item you're nesting. Inside this block, we define the specific properties.
00:15:24.080
Next, we’ll need to make a slight adjustment to our form. To keep this view flat, we can retain the 'fields for' method as it relates to collections within Reform. In the context of saving, we will not be required to implement custom logic because Reform will manage that for us.
00:15:51.790
Let’s also do away with strong parameters as they are no longer relevant. Those attributes will now be designated when we define properties in the form object, and Reform will disregard any parameters that aren't defined.
00:16:17.210
For saving, the process is different. Unlike the previous form object where we validated before saving, the controller will now handle the validation. In the controller, we call 'company_form.validate' and pass the parameters into what the form gives us, after which we call 'save'.
00:16:43.440
If something goes wrong, it typically manifests in how it looks on the surface. To resolve this discrepancy, we must instantiate the nested model in the controller. This process should work seamlessly.
00:17:13.619
However, Reform has its own method tailored for this scenario known as 'pre-populate.' All we need to do is incorporate a 'pre-populate' method in the collection, which we can call 'build_phone', although you can opt for any name you prefer.
00:17:38.230
This method functions as expected; it retrieves our collection, which is treated as an array, and we can append a new phone entry onto it. If everything is configured correctly, it should display the way we want. Generally, things are looking quite promising.
00:18:04.374
Now, to ensure the primary flag gets set to true when the form is saved, we simply add the property 'primary' and set a default.
00:18:29.494
However, if you run tests after doing this, you might notice that phones aren’t being saved. This is because Reform makes assumptions about nested data. Although it recognizes the parent object (company), it treats collections differently.
00:18:54.179
Knowing which model it needs to persist the collection to is crucial, and that’s why we need something called a 'populator.' You may now be asking yourself the difference between 'pre-populate' and 'populate.'
00:19:22.160
To comprehend this more easily, consider 'pre-populate' as actions in your new and edit requests tagged manually to prepare the form for rendering. It can fill in some default fields if desired. On the other hand, 'populate' comes into play during every validation, preparing the form for validation and ensuring that all data gets inserted in the right spot post-processing.
00:19:54.110
At the most basic level, to establish a setting in the collection, you will declare the 'populated with' option, alongside providing the class it maps to. This guarantees that upon validation, it'll persist into the phone class.
00:20:18.790
With the form object set up appropriately, we're only a few validations away from having this implementation running smoothly.
00:20:39.350
So that's our first form: two ways to implement it: the first through a custom roll-your-own method, and the second through the use of Reform. A side-by-side comparison reveals that there is indeed significantly less code required when utilizing Reform.
00:20:59.830
When it comes to the controller, the outcome is a bit of a wash; while the roll-it-yourself method stays cleaner and simpler, we no longer need to utilize strong parameters.
00:21:27.209
Next, we'll quickly go through the subsequent forms. In this form, we focus on adding and removing addresses, all handled within one model (the address model) by utilizing the controller to edit and update actions.
00:21:52.790
We create a new address form based on the previously determined company and call the 'pre-populate' method. Within our form, there are fields for addresses invoking a partial named 'address form' that incorporates all the necessary fields.
00:22:20.910
This also includes a tenant field named 'destroy' to mark whether the entity should be deleted, via a 1 or 0 flag—indicating whether to delete the address. Moreover, we implement a custom JavaScript helper that allows the user to add more addresses with the click of a button.
00:22:50.940
As part of the form object, we inherit from 'Reform::Form' and add our collection for addresses where 'destroy' is indicated as a virtual attribute, essentially not getting stored in the database. The pre-populated stage will operate similarly as before, ensuring at least one new address is displayed on the form.
00:23:19.579
We’ll write some custom logic for adding and removing addresses since we now need additional measures to handle fragment management. In previous stages, we directly populated empty lists and designated a class, but this time, we will need specific methods.
00:23:44.520
For managing duplicates within the address collection, we can ensure they are checked before creating duplicates, and subsequently proceed to remove any designated for deletion. We include validations, and our form is now set up to handle multiple addresses effectively.
00:24:21.460
For the final form, we observe that collected data is scattered across multiple models, including company information, user details, and account specifics. Since we previously saved to two companies, we only want to validate those attributes when necessary.
00:24:47.720
The form is structured without fields to keep it flat. Instead, we will pass the three models as a hash into our form object. Regarding the form object, we’ll utilize the composition module that comes with Reform.
00:25:19.280
We simply declare the company as the parent model to save to, then add in our properties accordingly, specifying their origin. Any potential property name clashes, such as identical record IDs, can be mitigated by giving them new property names within the form.
00:25:44.080
Our explicit inclusion of the objects into the form object through hashing means that every form now intuitively knows where to route each attribute during saving. In this final declaration, we add our validations to wrap it up.
00:26:02.760
So, what are the advantages of adopting form objects? First and foremost, they are model agnostic—this means you no longer need to take the view into account when modeling your data.
00:26:29.440
Additionally, you'll often find yourself with smaller, more focused models instead of unwieldy monolithic models. While I'm not advocating for entirely removing validations on your models, it is possible to rely on form validations alone to ensure everything is routed correctly without the need for any conditional validations.
00:26:51.900
This also allows the controller to maintain a more straightforward REST action format, making controllers easier to comprehend as they are simpler and not overloaded with logic.
00:27:10.720
On the topic of testing, form objects can be efficiently tested through integration tests comprehensively traversing the entire wizard. Additionally, testing individual form objects is made easier with Reform's structure, where you simply pass in a hash and validate the resulting outputs.
00:27:30.820
Furthermore, the simplicity of setting these tests up leads to faster execution. In scenarios where changes are requested by clients, form objects allow for straightforward adaptability. They can effortlessly accommodate alterations made to the order of a wizard or amendments in attribute visibility.
00:28:06.420
Finally, while you can easily use Rails defaults, employing form objects is ideal as systems grow more complex. They offer valuable tools that you can keep in your toolkit.
00:28:23.190
Give it a try on your next form. You may find it greatly enhances your development experience. Remember, they can be integrated into legacy applications seamlessly.
00:28:57.580
Thank you for joining me for this talk.
00:29:52.220
The question was about the stability of the Reform gem. I believe it is quite stable based on my experience with it. It is actively maintained.
00:30:10.150
While I'm not sure what future point releases will entail, I understand that Reform is currently at version 2 and there are plans for version 3.
00:30:35.060
The question arose regarding the decision to forgo validations in favor of using either models or forms exclusively. This decision depends on the specific application requirements.
00:30:58.690
For example, in scenarios where the company must have a name, it may make sense for model validation. However, when running validations in console or to create dummy entries, the approach could vary.
00:31:35.210
It often benefits the developer to be cautious in implementing validations, but in general, for non-critical data, form usages may suffice.
00:31:52.660
Well, thank you all very much for attending.