Nested forms consist of form fields representing at least 2 objects. For example one form with user and address attributes. In Ruby On Rails nested forms very often are built with
ActiveRecord::NestedAttributes::ClassMethods#accepts_nested_attributes_for and
ActionView::Helpers::FormHelper#fields_for.
Meanwhile it is proven going that path is a bad idea for several reasons. I fiddles with awful request parameters, burdens the model with false responsibilities and is hard to test. But there is an alternative, which decreases complexity: form objects.
Whenever:
- Nested forms
- Virtual model attributes
- Multiple varying forms for one resource
are required, form objects have to be considered, because they:
- Extract responsibility from the model objects
- Decouple models from forms
- Simplify forms
- Flatten parameter hashes and therefore simplify parameter check
- Simplify form handling when there are multiple different forms attached to one resource
The more a form is getting complex the more an appropriate form object is the solution.
For example a user form with address data has to be saved at once. First the
Address model (address.rb):
class Address < ActiveRecord::Base
validates :street, :number, presence: true
end
Starting from the original model object user.rb:
class User < ActiveRecord::Base
belongs_to :address
validates :name, presence: true
accepts_nested_attributes_for :address
end
a new form object class (user_address.rb) has to be created to achieve the goal:
class UserAddress
include ActiveModel::Model
validates :name, :street, :number, presence: true
delegate :name, :save, to: :user
delegate :street, :number, to: :address
def attributes= new_attributes
user.attributes = new_attributes.slice :name
address.attributes = new_attributes.slice :street, :number
end
def user
@user ||= User.new
end
def address
@address ||= user.build_address
end
end
The
UserAddress is a lightweight plain old Ruby object including the
ActiveModel::Model, which just means including validation and conversion stuff. To marry
User and
Address there is a reader acessor for each object and also delegators to their required attribute acessor methods. At least there is an writing accessor for all attributes which just fills both objects.
At first sight the form object seems to cost more effort, than just using
accepts_nested_attributes_for, but it offers more flexibility especially when the objects get more complex. Furthermore form logic baggage is extracted from the User model object, which is
NOT responsible for it. And the good thing is, the controller and the view stuff is way cleaner through the form object.
The original working but awful nested form (users/_form.html.haml)
= form_for @user do |user_form|
.text
= user_form.label :name
= user_form.text_field :name
= user_form.fields_for :address do |address_fields|
.text
= address_fields.label :street
= address_fields.text_field :street
.text
= address_fields.label :number
= address_fields.text_field :number
= user_form.submit 'Save'
could be way cleaner, if it looked like:
= form_for @user_address do |f|
.text
= f.label :name
= f.text_field :name
.text
= f.label :street
= f.text_field :street
.text
= f.label :number
= f.text_field :number
= f.submit 'Save'
Please note the
@user_address representing the form object and the straightforward form without any nesting.
Also compare the parameter hash from the nested:
"user" => {
"name" => "Chris",
"address_attributes" => {
"street" => "Main street",
"number" => "1"
}
}
to the flattened:
"user_address" => {
"name" => "Chris",
"street" => "Main street",
"number" => "1"
}
Finally even the original controller (controllers/users_controller.rb):
class UsersController < ApplicationController
def new
@user = User.new
@user.build_address
end
def create
@user = User.new
@user.attributes = user_params
@user.save
end
private
def user_params
params.require(:user)
.permit(:name, address_attributes: [:street, :number])
end
end
can be moved to the new resource (controllers/user_addresses_controller.rb):
class UserAddressesController < ApplicationController
def new
@user_address = UserAddress.new
end
def create
@user_address = UserAddress.new
@user_address.attributes = user_address_params
@user_address.save
end
private
def user_address_params
params.require(:user_address)
.permit(:name, :street, :number)
end
end
having an easier parameter check.
Even that small example illustrates how nested forms can be simplified with lots of nice side effects.
Further articles of interest:
Supported by Ruby 2.2.1 and Ruby on Rails 4.2.1