Form Objects

At today’s dev discussion in Boston, we discussed form objects. My notes follow.

What is a form object?

A form object exists specifically to be the object passed to form_for in your views. Often it aggregates data that is used to create multiple objets, though this doesn’t have to be the case – you may be collecting data for a single object but in a way that doesn’t cleanly map to the fields on your model.

** What’s Wrong with accepts_nested_attribtutes_for?**

The ‘Rails Way’ to aggregate data for multiple objects in a single form is through accepts_nested_attributes_for. This works reasonable well (though there are some gotchas), so what’s the problem?

  • It makes your model concerned with how the view is collecting data
  • It ties your view explicitly to the structure of all of the models and their relationships.
  • It increases the already large API of your ActiveRecord objects.
  • It can be confusing to reason about, particularly as you add more than 1 relationship or more than one level of relationships.

** An example form object **

Here’s an (annotated) example of a form object I wrote on my recent project. This object is used as the form on sign up and collects fields for user (username and email) and profile (zip code). This is a pretty simple example of a form object because it has very few fields and only two components its aggregating data for.

class Registration
  # Make this model play nice with `form_for`. Gives us validations, initialize, etc
  include ActiveModel::Model

  # Accessors for the fields we are exposing in the form
  attr_accessor :email, :password, :zip

  # This is an implementation detail of our authentication (Clearance). It's ultimately going to
  # sign this object in and that process expects a user object that has these fields available
  delegate :remember_token, :id, to: user

  # For this object to be valid, we want the child objects (user and profile) to be valid. This is
  # the validation method that does this.
  validate :validate_children

  # We tell registration it's a user. This will keep it pointing to Users controller and allow it to use
  # user translations
  def self.model_name
    User.model_name
  end

  # This is what our controller calls to save the user.
  def save
    if valid?
      # Create a transaction. If any of the database stuff in here fails, they will raise an exception
      # (because they are bang methods) and rollback the transaction.
      ActiveRecord::Base.transaction do
        user.save!
        profile.save!
      end
    end
  end

  private

  # Initialize the user object with the arguments that apply to user
  def user
    @user ||= User.new(email: email, password: password)
  end

  # initialize the profile object with the arguments that apply to profile
  def profile
    @profile ||= user.build_profile(zip: zip)
  end

  # the implementation of our validation method. We just delegate to our
  # two child objects which define their validations (presence on all fields in this case)
  def validate_children
    if user.invalid?
      promote_errors(user.errors)
    end

    if profile.invalid?
      promote_errors(profile.errors)
    end
  end

  # Errors on `user` or `profile` aren't helpful to us because `user.email` isn't on our form. `email` is however.
  # Take all errors from our child objects and promote them to the same field on the base object. This will make
  # them render properly on the form.
  def promote_errors(child_errors)
    child_errors.each do |attribute, message|
      errors.add(attribute, message)
    end
  end
end

That’s a pretty simple form object because it’s only concerned with the save action and exposes very few fields. It does however serve to illustrate some of the tricky bits of form objects that only get trickier as the form object increases in complexity.

** Validations **

How should you handle these? Should you delegate to the child objects as we have done in the registration or should the validations exist exclusively on the Model? Our feeling was that any validations that were exclusive to the form object (something the models might not care at all about but you care about in the context of this form) should live on the form object. We liked the idea of validations core to the model continuing to live on the model. That leaves two possible solutions for handling those in your form objects:

  1. delegate valid and promote like we did in the registration.
  2. Extract a validator object with the common validations and use it in both locations.

If you promote errors, what happens if an attribute not on your form (say, profile.first_name) becomes required in the base model? Now you will get an error trying to promote that error up to the parent form object because that attribute does not exist on the form object.promote_errors would have to be smart enough to move these to :base in some logical manner.

** Delegation overload **

Our example form object doesn’t have this problem, but in cases where you are dealing with existing, initialized objects passed in, you end up having to delegate a lot of methods (both accessors and potentially writers). We looked at one of the complicated examples of this that I won’t share here, but the solution there was to use method_missing to make the form object both a decorator and a form object. ActiveModel requiring initialize to take a hash made this more complicated than I wanted the process to be.

** Existing Libraries **

Form objects have been a fairly popular topic in the rails community lately so there’s been a few shots taken at libraries to make them simpler. We looked at these and felt they had some nice bits but had parts we also didn’t like. They either required writing the save logic in the controller or writing all the validations on the form object. We felt it might be worth taking a stab at doing a library that extracted the simple type of form object as shown above to see if that’s a pattern that is effective, reusable and expandable. It could handle the error promotion and perhaps have some niceties around defining the composite/child objects.

3 Likes

@derekprior Have you thought of recording the dev discussions? Or even a live feed with people pitching in on twitter? Or maybe just for learn subscribers…

Is that something you would have interest in?

I don’t think this is something we’ll do in the near term. We try to keep these chats friendly, informal, and comfortable for everyone. I suspect that if we opened it up we’d change that experience some.

That said, I think a number of these chats make for good exercises, blog posts, and weekly iteration topics and wouldn’t be surprised to see these topics pop up there.

The forum itself is also a very useful extension of the developer discussion. If you’ve got a question, a disagreement or a point to make we’d love to hear that here.

@derekprior This is a nice topic to see it in exercises, blog posts, and weekly iteration. I’ve started using it in my project. Few things I am not quite sure yet:

  • In case our form object depends on more objects from controller context like current_user, parent_object, cookies, …? Do you pass in these values by merging them with param object, then pass through form object’s constructor? Or define them as attr_accessor, then assign them after you instantiate the form object?
  • Do you usually test form objects in isolation without touching db?

Is it really the role of the form to know how to save models? Should it know how to build associations or is it just a representation of the form in the screen? What about to pass the form in a service object.

With my gem, it’s possible to override the save method and do what you want. On the other side, you must to initialize models elsewhere. Is it the role of the controller to manage all models? I have a ton of questions on this subject.

I’m really open to change my gem and improve it. In thoughtbot, you have a lot of experience. If you give me some ideas I will be very happy to change my gem to make it better.

About delegation, what you think on the solution I suggest to do properties :title, :name, on: :model_name?

Usually as a parameter to the form object initialization merged in by the controller with strong parameters.

Yes. I will test any methods added to the form object, including save. There’s also usually an integration test (or multiple if appropriate) backing those up.

Yes, I think the form object should know how to save itself in most cases. It’s more than a representation of the form on screen. I want the object to behave like a simple model would behave. Outside of the name of the form object, the controller isn’t aware that anything special is going on.

Here’s a quick gist of something I was kicking around. I don’t even like all of these ideas, but it shows what I was thinking I would like. An idea for a a basic form object that is for aggregating data on several models · GitHub

So, the form should be like an model. If I have complexe creation logic I will have this :

class RegistrationController < ApplicationController
  def create
    form = RegistrationForm.new(params)
    if form.valid?
      form.save
      #...
    else
      #...
    end
  end
end

class RegistrationForm
  #... validations, etc.

  def save
    RegistrationService.new(user).call
  end
end

class RegistrationService
  def call
    #... create the use, send email, etc.
  end
end

Are you agree?

Hello,

I tried to refactor a part of an application. It was like this controller->form & controller->service->form. Now, it is controller->form->service.

This is the before :

https://github.com/GCorbel/lescollectionneursassocies/blob/master/app/controllers/painting_orders_controller.rb
https://github.com/GCorbel/lescollectionneursassocies/blob/master/app/forms/painting_order_form.rb
https://github.com/GCorbel/lescollectionneursassocies/blob/master/app/services/painting_order_creator.rb

And this is the after:

https://github.com/GCorbel/lescollectionneursassocies/blob/refactor_forms/app/controllers/painting_orders_controller.rb
https://github.com/GCorbel/lescollectionneursassocies/blob/refactor_forms/app/forms/painting_order_form.rb
https://github.com/GCorbel/lescollectionneursassocies/blob/refactor_forms/app/services/painting_order_creator.rb

And this is the commit : https://github.com/GCorbel/lescollectionneursassocies/commit/91ff3ca7ebf35903491b0a37c08d377ddc62dc52.

What you think?

What is the best solution when you have a has_many association and want to use a gem like Cocoon to create the links to add more entries on the form?

I found myself having to write too many methods on the Form Object just for Cocoon. Should I no used the gem at all and write a solution in Javascript myself?

Just for a background, this are the models I have:

class Test < ActiveRecord::Base
  has_many :questions
end
class Question < ActiveRecord::Base
  belongs_to :test
  has_many :choices
  belongs_to :correct_answer, class_name: 'Choice'
end
class Choice < ActiveRecord::Base
  belongs_to :choice
end

So it’s a VERY complicated form, since I have two levels of nesting and also a circular dependency (Question has many Choices and one of them is the correct answer) there. I ended up using accepts_nested_attributes_for because it was easier (even after reading into the code base a lot of time to understand its behaviour), but I don’t like it. Am I completely wrong about this?

@derekprior Hi Derek! Quick question about this topic if you don’t mind. These Form Objects seem a lot like Presenters. Since the terminology on these is still a bit fuzzy to me I was wondering if they really are one and the same in such a case…
Thanks for your time!

How do you test the form object with all this controller dependency.