Adventures in Nested Forms

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

  1. accepts_nested_attributes_for is your friend. Don't be afraid of nested forms!
  2. Open source is an awesome way to contribute to the community while potentially learning a new skill yourself.