RailsConf 2017

Decouple Your Models with Form Objects

Decouple Your Models with Form Objects

by Andrew Markle

The video titled "Decouple Your Models with Form Objects," presented by Andrew Markle at RailsConf 2017, explores the use of form objects in Ruby on Rails applications to manage complex forms, particularly in scenarios where the data model does not align neatly with the form structure.

Key Points Discussed:

  • Challenges with Complex Forms:

    • Traditional Rails forms work well when the data model closely matches the form requirements.
    • Complicated setups, such as multi-step wizards or forms requiring dynamic attribute handling, can lead to unmanageable code and validations.
  • Downsides of Common Solutions:

    • Methods like using nested attributes, state machines, or storing fields in sessions can complicate code and introduce unnecessary dependencies between the model and presentation layers.
    • The speaker emphasizes that models should not handle view logic, which often leads to cluttered validation processes.
  • Introduction to Form Objects:

    • Form objects act as intermediaries between views and models, allowing developers to decouple the two.
    • They provide flexibility in defining attributes that do not need to correspond directly to a single data model, thus enabling better handling of complex user input scenarios.
  • Demonstration of Building Form Objects:

    • The speaker illustrates building a form object using Rails conventions and then refactoring the code to utilize the Reform gem from the Trailblazer framework.
    • Examples include creating forms for a dog-walking company wizard consisting of multiple steps where user details and addresses are entered across different forms.
    • Using Reform simplifies code by managing nested attributes and validation automatically, allowing for cleaner controllers and better management of form state.
  • Validation Improvements:

    • The video compares the manual creation of validation methods versus using the built-in features of the Reform gem to handle errors and manage nested models.
    • This promotes the idea that validation logic resides within form objects rather than affecting the models directly.
  • Testing and Extensibility:

    • Form objects are easy to test and modify, facilitating changes based on client feedback without significant rework.
    • This modular approach also aids in maintaining clean and focused code architecture.

Conclusions and Takeaways:

  • Form objects provide a powerful alternative to traditional model-binding in Rails, especially when dealing with complex forms.
  • They help maintain a separation of concerns, making the codebase clearer and reducing the complexity of validation.
  • The speaker encourages viewers to consider using form objects in situations where standard Rails forms start to become cumbersome.

Overall, the talk delivers valuable insights into best practices for managing forms in Rails applications, alongside practical coding examples that highlight the benefits of using form objects.

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.