This is going to be a long one, so grab your favorite beverage…
Problem
I have a fairly standard belongs_to and has_many model association. And I want to save nested attributes along with the parent when creating a new parent record. But, I don’t want the user to be able to submit duplicate values for the child records.
Sounds easy. That’s what I thought when I started.
My Solution
Models
We have an Organization and a SubUnit models, pretty standard stuff here.
1 2 3 4 5 6 7 8 |
lass SubUnit < ActiveRecord::Base attr_accessible :name, :organization_id belongs_to :organization validates :name, presence: true, uniqueness: { scope: :organization_id } end |
1 2 3 4 5 6 |
class Organization < ActiveRecord::Base attr_accessible :name, :sub_units_attributes has_many :sub_units, dependent: :destroy accepts_nested_attributes_for :sub_units, allow_destroy: true, reject_if: proc { |a| a["name"].blank? } validates_presence_of :name end |
Controller
The controller is simple (standard actions removed). Keep scrolling down.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# controllers/organizations_controller.rb class OrganizationsController < ApplicationController def new @organization = Organization.new @organization.sub_units.build end def create @organization = Organization.new(params[:organization]) if @organization.save redirect_to @organization, notice: 'Organization was successfully created.' else render :new end end end |
View
Now it’s getting interesting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# organizations/new.html.haml = form_for @organization, :html => { :class => 'form-horizontal' } do |f| .row-fluid .span6 .well %h2 Organization Info .control-group = f.label :name, :class => 'control-label' .controls = f.text_field :name, :class => 'text_field input-medium' .span6 .well.sub-units %h2 SubUnit Info = f.fields_for :sub_units do |f_sub| = render 'sub_unit_fields', f: f_sub .control-group.add-sub-unit .controls = link_to 'add a SubUnit', '#', class: 'btn btn-primary add-sub-unit-button' #new-sub-unit-fields.hidden = f.fields_for :sub_units, SubUnit.new, child_index: 'new_sub_unit' do |f_sub| = render 'sub_unit_fields', f: f_sub .span12 .form-actions = link_to 'Create Organization', '#', class: 'btn btn-primary create-organization-button' = link_to t('.cancel', :default => t("helpers.links.cancel")), organizations_path, :class => 'btn' |
1 2 3 4 5 6 7 |
# _sub_unit_fields.html.haml .control-group = f.label :name, class: 'control-label' .controls.sub-unit = f.text_field :name, class: 'input-mini' = f.hidden_field :_destroy = link_to "remove", '#', class: 'btn btn-warning btn-mini remove-sub-unit-button' |
Your form should look like this.
Polluting Links with Javascript
Like everyone else who has started down the road to nested attribute bliss, you have watched Ryan Bates’ Railscast on Complex Forms. It’s superlative.
But there’s one thing I didn’t like… the heavy handed approach of using link_to_function. Especially when including a fair amount of escaped html into an onclick script. I’m not saying it’s wrong, just that I believe html should stay where it belongs… in the DOM. Heck, they even created a gem nested_form.
I think we can make it simpler, painless, elementary.
That’s where the following little ditty comes in handy. We are creating the exact same field structure that is used for entering a SubUnit, but hiding the html so that we can use JavaScript to copy-and-append to the visible entry area.
1 2 3 |
#new-sub-unit-fields.hidden = f.fields_for :sub_units, SubUnit.new, child_index: 'new_sub_unit' do |f_sub| = render 'sub_unit_fields', f: f_sub |
Line #1: hide the div.
Line #2: use fields_for to generate the proper field id and name attributes so that it is submitted as a child object to our Organization. Notice the SubUnit.new? That prevents this block from iterating over all the @organization.sub_units that may be built, either as a new object (controller line #5), or when re-rendered due to a validation error with @organization.
Line #2: don’t forget the child_index: ‘new_sub_unit’ option! Without it, rails will attempt to index the fields id and name attributes with numbers, which in this case would be 0 (zero), and mess up the prior sub_unit fields. IOW, you will end up with two params[:organization][:sub_units_attributes][0] elements.
If you don’t understand all the details, take a few minutes and read the ActiveRecord::NestedAttributes::ClassMethods.
Now for some JS magic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
jQuery -> $('.well.sub-units').on('click', 'a.remove-sub-unit-button', -> remove_sub_unit(@) ) $('.add-sub-unit-button').click -> add_sub_unit(@) add_sub_unit = (link) -> new_sub_unit = $(".hidden#new-sub-unit-fields").html() new_id = new Date().getTime() regexp = new RegExp("new_sub_unit", "g") $(link).closest('.add-sub-unit').before(new_sub_unit.replace(regexp, new_id)) $(link).closest('.add-sub-unit').prev().find('input[type=text]').focus() remove_sub_unit = (link) -> $(link).prev("input[type=hidden]").val("1") $(link).closest(".control-group").hide() |
Pretty simple, bind to the click events of the add-sub-unit-button and remove-sub-unit-button to functions.
add_sub_unit function copies the new-sub-unit-fields div, replacing the text of ‘new-sub-unit’ in the id and name attributes of the input/label elements with an integer.
remove_sub_unit function hides the input field group, and set the ‘_destroy’ hidden field to “1” (‘true’ also works) so that it will not be created when the form is submitted.
Inspiration for this JavaScript goes to Ryan Bates.
Status Check
At this point we have a parent model, Organizations, with nested attributes for children SubUnits. When creating a new Organization, you can add and remove SubUnits. The entire relationship is submitted with a single form post.
Eliminate Duplicates in the DOM
Everyone else does it in model validations, manipulation of the params hash, or some other convoluted way. But I decided to eliminate duplicates before the submission of the parent form. Pretty easy, actually.
1 2 3 4 5 6 7 8 9 10 11 |
jQuery -> $('a.create-organization-button').click -> # remove duplicate form values, then submit the form uniqueNames = [] $.each($('.controls.sub-unit input[type=text]'), (i, el) -> if $.inArray($(@).val(), uniqueNames) == -1 uniqueNames.push($(@).val()) else $(@).parent().find('input[type=hidden]').val("true") ) $(@).closest('form').submit() |
When the Create Organization button is clicked, the above JavaScript loops through all the SubUnit form field elements, marking duplicate entries hidden ‘_destroy’ field with ‘true’, thus preventing them from being saved. Could you just remove the elements? Sure.
Inspiration for the JavaScript remove duplicates in an array to Roman Bataev in this post.
If you watch your logs, you’ll see that all the SubUnits attributes are submitted, but only those marked with the hidden field ‘_destroy’ == ‘false’ will be created.
1 2 3 4 5 6 7 8 9 10 |
Started POST "/organizations" for 127.0.0.1 at 2012-12-21 16:32:39 -0700 Processing by OrganizationsController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"Dkphnf9nkc+5cxqRgo7mkQLcz2nvxHIUxezlOPA85cA=", "organization"=>{"name"=>"My Org", "sub_units_attributes"=>{"0"=>{"name"=>"Unit1", "_destroy"=>"false"}, "1356132751848"=>{"name"=>"Unit1", "_destroy"=>"true"}, "1356132755193"=>{"name"=>"Unit2", "_destroy"=>"false"}, "new_sub_unit"=>{"name"=>"", "_destroy"=>"false"}}}} (3.5ms) begin transaction SubUnit Exists (0.4ms) SELECT 1 AS one FROM "sub_units" WHERE ("sub_units"."name" = 'Unit1' AND "sub_units"."organization_id" IS NULL) LIMIT 1 SubUnit Exists (0.1ms) SELECT 1 AS one FROM "sub_units" WHERE ("sub_units"."name" = 'Unit2' AND "sub_units"."organization_id" IS NULL) LIMIT 1 SQL (28.4ms) INSERT INTO "organizations" ("created_at", "name", "updated_at") VALUES (?, ?, ?) [["created_at", Fri, 21 Dec 2012 23:32:39 UTC +00:00], ["name", "My Org"], ["updated_at", Fri, 21 Dec 2012 23:32:39 UTC +00:00]] SQL (2.1ms) INSERT INTO "sub_units" ("created_at", "name", "organization_id", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Fri, 21 Dec 2012 23:32:39 UTC +00:00], ["name", "Unit1"], ["organization_id", 6], ["updated_at", Fri, 21 Dec 2012 23:32:39 UTC +00:00]] SQL (0.2ms) INSERT INTO "sub_units" ("created_at", "name", "organization_id", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Fri, 21 Dec 2012 23:32:39 UTC +00:00], ["name", "Unit2"], ["organization_id", 6], ["updated_at", Fri, 21 Dec 2012 23:32:39 UTC +00:00]] (48.5ms) commit transaction |
The astute reader will also notice that the SubUnit model is attempting to validate the uniqueness of the name attribute, scoping it to a non-existant Organization. I admit it would be best to skip this validation, but I will leave that exercise to you, humble reader.
Sample App
What? You wanted a demo app on the GitHubs? You shall be rewarded, post haste.
Synopsis
The Good
- Submission of a parent Organization with children SubUnits utilizing nested attributes.
- Duplicate SubUnits are removed.
- Darn simple, easy to reproduce. Pretty darn unobtrusive, too.
- No funky helpers to jack some html into a link element.
- Is it better than Ryan Bates’ solution? No. Just simpler.
The Bad
- Only works on creating a new Organization. Does not work for editing an Organization with the ability to add additional SubUnits or destroy existing SubUnits. Future post?
- My JavaScript is not abstracted to make it easily reusable. Fork it, and submit a patch if you so desire.
- The SubUnit validation should be skipped when creating through an Organization.
- No fallback if JavaScript is disabled.
Additional Reading
- StackOverflow: validates_uniqueness_of in nested model rails, a great discussion of how to handle scoped validation when editing existing child attributes.
- Rails issue:
validates_uniqueness_of
andaccepts_nested_attributes_for
only validates existing records, macfanatic shows a nice way to handle the same issue. - Rails pull request: Validates associated uniqueness for nested attributes. The paint is still wet on this one. Hopefully is stirs some discussion.
- Ryan Bates’ gem: nested_form.