Adventures in Nested Forms
Margaret Williford, Former Developer
Article Categories:
Posted on
I explored open source for the perfect opportunity to grow my complex form-making skills. In this article, I share tips for using Active Record's accepts_nested_attributes_for effectively.
A few weeks ago, I had the opportunity to spend a few days between projects to contribute to open source. I've always wanted to contribute to open source, but wasn't sure where to start, having only entered the world of development less than a year ago. Viget's professional development time gave me the opportunity to check something off my bucket list, while also learning a new skill. I decided to look into Rails Girls Summer of Code (RGSOC). RGSOC is "a global fellowship program for women and non-binary coders. Students receive a three-month scholarship to work on existing Open Source projects and expand their skill set." Their site is also open source, so working on an open source site that supports open source work was sort of an Inception. It seemed like a great place to get my feet wet in the community.
I poked around on their GitHub Issues to find something both useful to them and useful to me. I found a perfect match! In one of our client projects, I had worked a bit with Rails' accepts_nested_attributes_for
but didn't feel like I had a deep understanding of how it was working. RGSOC wanted to break out their address
field from the User
model into its own model with street
, city
, state
, zip
, country
, etc fields. Simple enough. But the User
edit
form brought two interesting challenges: How to work around PostalAddress
model validations and how to remove (destroy
) an address.
Problem 1: Validations. #
Sure we're doing some back end rearranging but, we don't want the user to feel that this attribute is handled differently than other attributes. So, the edit user profile page should look the same. What we'll need is some Rails magic to save the postal address into a separate table. Enter accepts_nested_attributes_for
. Adding this handy class method to a model allows you to save attributes on associated records through the parent. This is incredibly helpful when working on a nested form. We'll also need to pass allow_destroy: true
flag. (More on that later.)
For the sake of simplicity, we'll assume here that a user can only have one address associated with their account. accepts_nested_attributes_for
assumes nothing about association relationships.
class User
has_one :address
accepts_nested_attributes_for :postal_address, allow_destroy: true
end
With this setup, you can now nest your User
form to accept attributes for a PostalAddress
. In RGSOC's case, a user was not required to have an address. This made the form a bit trickier. Without passing a required: false
flag, the form would error out if a user didn't enter any address information.
= simple_nested_form_for @user do |f|
= f.input :name
= f.input :email, required: true
= f.input :phone
= f.simple_fields_for :postal_address_attributes, @user.postal_address do |pa|
- if @user.postal_address
= pa.input :id, as: :hidden, input_html: { value: @user.postal_address.id }
= pa.check_box '_destroy'
| Remove Address // more on this later!
= pa.input :line1, required: false
= pa.input :line2, required: false
= pa.input :city, required: false
= pa.input :state, required: false
= pa.input :zip, required: false
= pa.input :country, required: false, include_blank: true
= f.submit 'Save'
class UsersController < ApplicationController
def new
end
def edit
@user = User.find(params[:id])
end
def create
@user = User.new(user_params)
if @user.save
redirect_to '/dashboard'
else
render :new
end
end
def update
if @user.update_attributes(user_params)
redirect_to '/dashboard'
else
render :edit
end
end
private
def user_params
params.require(:user).permit(
:name,
:email,
:password,
postal_address_attributes: [:id, :line1, :line2, :city, :state, :zip, :country, :_destroy]
)
end
end
Problem 2: Destroying Dependents #
What happens when a user wants to delete the address? The _destroy
key word is another part of the magic that you get with ANAF. When you pass _destroy: true
in your attributes hash along with the child id
, Rails will destroy the child object. In the form above, the id
is being passed as a hidden input and the _destroy
flag is set as a checkbox. This will not work without the id
being passed. This won't occur until the parent object (User
in this case) is saved. If there is any error saving the parent, the child won't be destroyed. I included a checkbox to remove the attributes, but there are other options for doing this. You could also add a link to Remove Address
, which would immediately destroy the child and redirect the user. This potentially would be confusing to a user who wasn't expecting to leave the edit
page.
TL;DR #
accepts_nested_attributes_for
is your friend. Don't be afraid of nested forms!- Open source is an awesome way to contribute to the community while potentially learning a new skill yourself.