Expert Software Development, Staffing & Consulting


Rails 3.2: A Nested-Form Demo, Part 1: All Wings Report In!

Written by Jeff Johnson on Jan 19 2013 - 14 Comments

Background

I’ve been working on a Rails 3.2 project for the last several months. While it’s been a blast to learn a new technology stack (I’ve spent most of my career working with .NET), there have definitely been some bumps in the road. In particular, I’ve spent the last couple weeks working on persisting a model that has a parent-child relationship. The bulk of that time was spent poking around the inter-webs, looking for tips and tricks to accomplish my goal. During my inter-web search, I couldn’t find a single post (or series of posts) that had a complete example or tutorial on how to persist a model that has a parent/child relationship. My Google-fu might not be as good as some other developers out there, but I searched pretty hard. What I found were bits and pieces scattered about (primarily on my savior, Stackoverflow). After a LOT of trial and error (mostly the latter), I finally have it working. So I thought I’d collect my notes as a series of posts. FYI -  this started as one post, but got WAY too long. Even for me.

Objective/Goals

Reading is real neat, and I do a whole lot of it. I tend to learn by doing, so reading only gets me halfway there. To that end, we’ll build a demo application that ultimately addresses the following:

  • Allows a user to save a model that has a parent/child relationship.
    • The operation will allow us to save the parent object as well as insert/update/delete new child objects that are associated to the parent.

Index

Shown below are links to the other articles in this series:

The Application

Most demo apps I see deal with users, customers, security groups and the like. That’s all well and good, but I thought it’d be fun to work on a demo app about something I like – Star Wars! My favorite aspect of Star Wars is the ships, particularly the fighters. So, let’s build a simple database app that will allow us to maintain data about the fighters in the Star Wars universe. The idea behind the demo application is fairly simple: it’s a Star Wars Starfighter Recognition Guide (albeit a VERY simplified one).

Initially, the concept for our initial application is very simple:

  1. There are Ships.
  2. Ships have zero to many Pilots (i.e. characters/personalities who fly the type of ship being viewed).

For example: Garven Dreis, Wedge Antilles, and Biggs Darklighter all fly the T-65 X-Wing.

This application will allow a user to create/edit/delete a Ship. While creating a Ship, a user has the option of creating records for the Pilots that fly the Ship being created. When editing an existing Ship, a user has the ability to:

  1. Create a new Pilot.
  2. Edit an existing Pilot.
  3. Remove a Pilot from the list.

A user can conduct any number of the changes cited above prior to saving. When the form is submitted, we expect Rails/ActiveRecord will do the right thing. It will:

  1. Destroy the Pilots that were removed.
  2. Add the Pilots that were created.
  3. Update the Pilots that were changed.
  4. Update the Ship attributes that were changed.

NOTE: This series of posts assumes some basic knowledge of Ruby and Rails. I highly recommend going through the Rails Tutorial – it’s a great introduction to the technology.

Righty-o – let’s get started.

The Model

For starters, let’s take a look at the domain model we’ll be working with for this demonstration.  We’ll keep things simple by having a straightforward parent/child relationship:

  • Ship: This will be our “parent” object – it represents some basic information about a starfighter in the Star Wars universe.
  • Pilot:  This will be our “child” object – it represents a person who is rated to fly the starfighter.

Here’s a picture of what our domain model we’ll be working with (thanks, RubyMine!):

the Ship and Pilot model

Moving on, let’s take a look at the code for each object in our domain model:

app/models/ship.rb:

class Ship < ActiveRecord::Base   
  attr_accessible :armament, :crew, :has_astromech, :name, :speed   
  attr_accessible :pilots_attributes   

  has_many :pilots   
  accepts_nested_attributes_for :pilots,                                 
                                :reject_if => lambda { |attrs| attrs.all? { |key, value| value.blank? } },
                                :allow_destroy => true

  #I find your lack of validation disturbing...
end

I’d like to call your attention to the attr_accessible :pilots_attributes – that’ll be important later.

app/models/pilot.rb:

class Pilot < ActiveRecord::Base
  belongs_to :ship
  attr_accessible :call_sign, :first_name, :last_name, :ship_id

  #I find your lack of validation disturbing...
end

The accepts_nested_attributes_for Method

According to the documentation, the accepts_nested_attributes_for method “Defines an attributes writer for the specified association(s).” What does that mean, exactly? That means that our Ship model can take in attributes for any of its associated Pilots and update them. To use accepts_nested_attributes_for, we point it at one of our associations. Right now, we only have one association – has_many :pilots – so the choice is pretty easy. Armed with the accepts_nested_attributes_for :pilots, we can:

  • add new Pilots to our Ship.
  • update our Ship's existing Pilots.
  • there is a special attribute (_destroy) that will allow us to mark certain Pilots to be deleted from our Ship(more on this in a bit).

How accepts_nested_attributes_for Works

What’s going on behind the scenes (as I understand it, anyway) is this: When you add an accepts_nested_attributes_for method to your model, a writer is also added to your model. That writer will be named as follows [the name of your association]_attributes. In our case, since we have a accepts_nested_attributes_for :pilots method, the writer will be named pilots_attributes (note the pluralization of Pilot).

accepts_nested_attributes_for Options

Notice that our Ship model also has an attr_accessible :pilots_attributes accessor set up. This allows us to easily pass our Pilot data to the pilots_attributes writer that was created as a result of the accepts_nested_attributes_for implementation. With the pilots_attributes in our attr_accessible list, we won’t get the dreaded “Can’t mass-assign” error.

NOTE: having your accepts_nested_attributes_for writer available for mass-assignment might not be a good idea for your real-life application. Take careful consideration when adding your model’s attributes to the attr_accessible list.

Next, we have the :reject_if option. This option allows us to specify a method (using a symbol or an anonymous method) to determine if a Pilot record should be built. Whatever code is executed by the :reject_if option should return true or false. In our case, we don’t want to build a Pilot record if all the attributes are empty. This will prevent ActiveRecord from saving blank/empty rows in our Pilots table.

Finally, we have the :allow_destroy option. This option can be set to true or false, and it does pretty much what it says. If we have :allow_destroy => true (as we do in our case), ActiveRecord can delete the Pilots that have their :_destroy attribute set to true.

You can check out the documentation and the available options for the accepts_nested_attributes_for method here.

What a POST Looks Like From the UI

I realize I’m skipping ahead a bit, ’cause we haven’t even talked about the UI. However, I thought it would be beneficial to see how the data for nested objects (i.e. our Pilots) is sent across the wire. After watching a couple POSTs, it’s really easy to see what’s going on with all the setup we did on our models above.

Scenario 1 – A User Creates a New Ship With No Pilots

If a user only creates a Ship and no Pilots (i.e. the ships#create method is called on the ships_controller), the data contained in the POST will look like this (taken and “prettified” a bit from Firebug):

&ship[name]=TIE+Fighter
&ship[crew]=1
&ship[has_astromech]=0
&ship[speed]=100
&ship[armament]=2+laser+cannons

As you can see above, we’re only passing attributes for our Ship object. The controller will take these parameters and build a new Ship object. Here’s the SQL that was executed when the controller saved our new Ship to the database:

INSERT INTO "ships" ("armament", "created_at", "crew", "has_astromech", "name", "speed", "updated_at") 
VALUES (?, ?, ?, ?, ?, ?, ?)
[["armament", "2 laser cannons"], 
["created_at", Tue, 15 Jan 2013 19:04:13 UTC +00:00], ["crew", 1], 
["has_astromech", false], 
["name", "TIE Fighter"], 
["speed", 100], 
["updated_at", Tue, 15 Jan 2013 19:04:13 UTC +00:00]]

Scenario 2 – A User Creates a New Ship With a Couple Pilots

Now our user is getting a little ambitious, but thanks to the setup we did on our model we’re ready. In this case, a user has created a new Ship with a couple Pilots associated. Shown below is a POST that creates a new Ship with two Pilots:


&ship[name]=TIE+Fighter
&ship[crew]=1
&ship[has_astromech]=0
&ship[speed]=100
&ship[armament]=2+laser+cannons
&ship[pilots_attributes][1358277455305][first_name]=Cive
&ship[pilots_attributes][1358277455305][last_name]=Rashon
&ship[pilots_attributes][1358277455305][call_sign]=Howlrunner
&ship[pilots_attributes][1358277455305][_destroy]=false
&ship[pilots_attributes][1358277658761][first_name]=Dodson
&ship[pilots_attributes][1358277658761][last_name]=Makraven
&ship[pilots_attributes][1358277658761][call_sign]=Night+Beast
&ship[pilots_attributes][1358277658761][_destroy]=false

Let’s take a look at the pilots_attributes parameters – they correspond to our :pilots_attributes accessor. Each set of pilots_attributes has four attributes grouped by a “unique id” of sorts (we’ll see how the unique id is generated in a future article). In our controller, when we call Ship.new(params[:ship]), ActiveRecord can build the Pilots that correspond to the Ship in the ships#create method. That’s because we have declared attr_accessible :pilots_attributes in our Ship model. We can also see the special _destroy attribute, which is set to false in both cases. That means we want to add these Pilots to our database.

For the sake of completeness, here’s the create method in the ships_controller:
app/controllers/ships_controller.rb:

def create
  @ship = Ship.new(params[:ship])

  if @ship.save
    redirect_to(ships_path, :notice => "The #{ @ship.name } ship has been saved successfully.")   
  else
    render(:new, :error => @ship.errors)
  end
end

Again, here’s the SQL that was generated by ActiveRecord to save our Ship and its associated Pilots:

(0.1ms) begin transaction
SQL (0.4ms) 
INSERT INTO "ships" ("armament", "created_at", "crew", "has_astromech", "name", "speed", "updated_at") 
VALUES (?, ?, ?, ?, ?, ?, ?) [
["armament", "2 laser cannons"], 
["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], 
["crew", 1], 
["has_astromech", false], 
["name", "TIE Fighter"], 
["speed", 100], 
["updated_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00]
]

SQL (0.7ms) 
INSERT INTO "pilots" ("call_sign", "created_at", "first_name", "last_name", "ship_id", "updated_at") 
VALUES (?, ?, ?, ?, ?, ?) [
["call_sign", "Howlrunner"], 
["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], 
["first_name", "Cive"], 
["last_name", "Rashon"], 
["ship_id", 8], 
["updated_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00]
]

SQL (0.3ms) 
INSERT INTO "pilots" ("call_sign", "created_at", "first_name", "last_name", "ship_id", "updated_at") 
VALUES (?, ?, ?, ?, ?, ?) [
["call_sign", "Night Beast"], 
["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], 
["first_name", "Dodson"], 
["last_name", "Makraven"], 
["ship_id", 8], 
["updated_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00]
]
(2.2ms) commit transaction

Summary: Look at the Size of That Thing!

We’ve barely impacted on the surface of our Starfighter Recognition Guide, but we’ve managed to take a fairly detailed look at how to:

  • Establish a parent/child relationship in our model.
  • Set up our “parent” model to accept data for any “child” objects, allowing us to save the “parent” and all its “children” at once.
      We’ve also used the Force to look ahead at our UI and the data it will send to our controller when the user submits the form. This peek into the future has exposed some of the internal plumbing that goes on when Rails builds/saves a model with a parent/child relationship.

In our next installment, we’ll take a good long look at the controller and the UI helpers for our Starfighter Recognition Guide. We’ll add some screens that will allow our user to perform CRUD operations on our Ships and Pilots.

Stay tuned…

Part 2:  Accelerate to Attack Speed! >>

14 Comments

  • Kevin Browne / July 16, 2013

    Thanks for your excellent series of articles, Jeff. It was an outstanding explanation of your approach and a comprehensive discussion of your solution! I’m trying to adapt the approach – adding a new nested-attrib record via a modal. I appreciate if you are not in a position to support it, but I wanted to see if you might speculate? I’m getting a “Uncaught TypeError: Cannot read property ‘ownerDocument’ of undefined” in the pilot-adding js code. Since I don’t know exactly how your Javascript works, I’m wondering if this could be:
    a) I’m not showing all the fields in the record (I apply defaults to some fields)
    b) I am trying to use a select field along with two inputs (is it the same class selector?)
    c) I am displaying several ‘pilot tables’ in the view (imagine it’s squadrons nesting ships nesting pilots).

    I think the issue occurs when trying to create the new row – any thoughts?

    Thanks!

    Reply
    • Jeff Johnson / July 16, 2013

      Hi Kevin. First off: thank you so much for the kind words, I’m glad you enjoyed this series! Without seeing the code, it’s a bit difficult to assess exactly what’s happening. However, I have a few thoughts:

      It sounds like some elements are trying to be added to a “parent” element that doesn’t exist. This could be during the population of your select field. The same thing could be happening you’re storing the default values in hidden fields and adding them to a form (or something). Assuming you’re using jQuery, I’d check all your append and appendTo calls.

      Is there a lot of AJAX going on? If you’re building several DOM elements via AJAX results (like during the ajax:success event), that might be causing your issue. For example, the javascript is trying to append elements to another element that doesn’t exist yet. I tend to screw this up a lot.

      The debugging tools in the browser (FireBug for Firefox or Chrome’s debugging tools) might be able to provide some insight to where the javascript is throwing the ownerDocument exception. Hopefully, the debugger will point to the missing element

      Also, I ran into this article, which might be what you’re running into.

      Finally, if you can set up a jsfiddle that re-produces your issue, I can try to take a look and see if I can help identify what’s going on.

      Reply
      • Kevin Browne / July 17, 2013

        I appreciate the reply, Jeff, especially as I didn’t give you much to go on! I’m way new to jQuery and JS using DOM generally, but your response gave me some ideas on how to track down what’s amiss. It’s definitely creating the new nested-attrib, so I am looking into where it’s losing the ‘owner’. I backed out some of my partials to see if it’s the same issue they talk about on Jeuweb, but I’m thinking it’s core to adding the table row.
        I’ll continue running that down for now. Thanks again!

        Reply
        • Jeff Johnson / July 17, 2013

          Sure thing, Kevin! I’m sorry I couldn’t be more helpful. Issues like these can be pretty tricky to track down. Here are another couple things that might help:

          1. Check all your ids and classes on your jQuery selectors. Maybe you’re trying to add a TR element to a TABLE that is misnamed in the addRow method somewhere? If I had a nickel for every time I left off a # or . on one of my jQuery selectors, well… let’s just say I could retire early to a wee island in the sun. This is unlikely to be your issue, but it’s something that’s tripped me up about a bajillion times.

          2. You might try changing your append and/or appendTo calls to html. Theoretically, this will show you what’s being rendered to the page. Maybe that’ll shed some light on what’s going on.

          3. The browser’s debugging tools can be a bit daunting at first, but they really are a godsend. I found this slide presentation to be incredibly helpful (and more than a little entertaining!).

          If you can post the code that’s causing you grief somewhere (and I totally understand if you can’t), maybe I can take a look and lend a hand. Best of luck, and once again I’m sorry I couldn’t be more helpful.

          Reply
  • Kevin Browne / July 17, 2013

    So I should have checked this earlier, but I’m using a later version of jquery-rails (3.0.2), which installs jquery 1.10.x. Your app uses 2.1.4, which uses jquery 1.8.3. According to the jquery site, sizzle has been totally re-written, on top of whatever other differences there are between the ie-compat(1.x) and non-ie-compat(2.x-trending) versions of jquery. I’m going to play around with their migration plug-in, replacing the jquery-rails version in gemfile, etc.

    Reply
    • Jeff Johnson / July 18, 2013

      Hey there, Kevin – I think I have this sorted out. I grabbed the “nested-form” source from GitHub and upgraded the gems etc. to all the latest and greatest using bundle update. After scratching my head for a bit, here’s what I found:

      The issue is in the rowBuilder.buildRow() method (line 87 in ships.js). It looks like the ownerDocument exception is being thrown on line 93, when the TD elements are being added to the TR we’re trying to build. I made a couple minor changes to the buildRow method, which now looks like this:


      var rowBuilder = function() {
      ...
      // A private method for building a w/the required data.
      var buildRow = function(fields) {
      var newRow = row.clone();

      // Instead of returning an array of TD elements, we'll add the TD directly to the newRow
      // object. This resolves an "ownerDocument undefined" exception thrown w/jQuery 1.10.1.
      $(fields).map(function() {
      $(this).removeAttr('class');
      var td = $('<td/>').append($(this));
      td.appendTo(newRow);
      //return td;
      });//.appendTo(newRow);

      return newRow;
      }
      }();

      I’ve also created a JS Fiddle where you can see a stripped-down version of the buildRow method. Just click the Add a Row button and watch the magic! Feel free to mess with it to your heart’s content.

      If you’re still having trouble, please don’t hesitate to leave a comment and I’ll do what I can to help out. Thanks for all your patience!

      Reply
      • Kevin Browne / July 18, 2013

        Seriously, I’m the one thanking YOU for your patience! I can’t believe you went to all that trouble – it definitely helps. What I found in going back to jquery 1.8.3 — and what I’m seeing now with your new rowBuilder — is that the script returns the new row without ownerDocument errors as you note. My issue is that, for some reason, it is only picking up the hidden “destroy” field, so it’s only adding one in the new row, and it appears empty because it has a hidden field. I’ve been stepping through the jQuery code, and it *seems* to be the result of not properly reading my modal. As it’s getting ready to collect all of the fields, the selector is set properly, but the context in your code is set to ‘body.modal-open’, while mine is just ‘body’. I’ve backed out everything complex and am just using fields. Will be re-reading the Bootstrap docs to see if there is something I’m doing elsewhere that might be confusing it.

        Thanks again for your generous assistance to an RoR newbie!

        Kevin

        Reply
        • Jeff Johnson / July 19, 2013

          Howdy, Kevin! Sorry for the delayed response. I’m glad you were able to resolve the ownerDocument error; now it sounds like you’re tackling a number of other issues. Let’s see if I can help get you pointed in the right direction:

          …only picking up the hidden destroy field…
          Hmm… that’s odd. I tested the code in my previous comment, and it seems to be working. You clearly have the TABLE‘s selector set properly, since you’re getting a new row with just a _destroy hidden field. Here’s what I suggest:
          1. Make sure you’re passing what you’re expecting to the second parameter of the rowBuilder.addRow() method.
          2. in the buildRow method, change this:

          $(fields).map(function() {
          $(this).removeAttr('class');
          var td = $('<td/>').append($(this));
          td.appendTo(newRow);
          });

          to this:

          $(fields).map(function() {
          $(this).removeAttr('class');
          var td = $('<td/>').append($(this));
          console.log(td);
          td.appendTo(newRow);
          });

          You should be able to see the TD elements created in the map method in the browser’s console. From there, you should be able to drill down into the TD elements to see what (if anything) is in there. If the TD elements aren’t being created, I suspect the fields argument is empty or isn’t an array.

          …the context in your code is set to ‘body.modal-open’, while mine is just ‘body’…
          So… the modal isn’t popping up at all, right? This is probably a dumb question, but are all the bootstrap js files being included? Check to make sure that bootstrap-modal.js is being included.

          Also, you might want to check the app/assets/javascripts/application.js file and take a look at the add_fields method. The last line (line 27 in mine) has the ID of the modal DIV that contains the data entry fields. In the demo app, the ID is #new-pilot-fields. When I changed this:

          $('#new-pilot-fields').modal('show');

          to this:

          $('#new-pilot-fields-foo').modal('show');

          I saw the behavior I think you’re experiencing. That is, the .modal-open class was not added to the body element. If the ID looks right in the application.js add_fields method, then it might be worthwhile to double-check the ID of your modal DIV.

          In other news, I’ve updated part thee of this series with the solution to the ownerDocument error you struggled through. I’ve also checked the change into GitHub.

          Reply
  • Kevin Browne / July 22, 2013

    I did check all of those. Since I am loading bootstrap through the bootstrap-rails gem, it automagically pulls in all the correct js files. I’m going back through my application layout to see if some things I’m doing there may be interfering with the ‘add pilot’ modal. I found that when I refer directly to the fields like this:
    var inputFields = $(‘#new-battalion-fields :input’).not(‘:button’);
    …the javascript detaches the fields from the modal and inserts them in the table as expected. I have to adapt this part for the fact that I have the equivalent of 1-5 “ships” per page, each with its own “pilots table”, so adding a pilot currently adds the fields to all of the “ships”. But I should be able to add a counter to the table names, and pass that into the javascript code.

    Can’t tell you how cool the framework you created is, though. When I get a bit more working right, I’ll actually post it onto github if you want to take a look!

    Kevin

    Reply
    • Jeff Johnson / July 24, 2013

      Well, it certainly sounds to me like you’re on the right track! Adding a counter (or some other mechanism) to uniquely identify your tables should do the trick. I’d love to see what you’ve put together, so feel free to post it on GitHb.

      Reply
  • Chris / September 1, 2013

    I have a problem appending the “Remove” icon ..
    At this line: rowBuilder.link.appendTo($(‘tr:last td:last’));
    this way, the icon statys just on the last row.. the other rows stay without the icon. If I create another row, the icon goes to the new row and the old one gets any icon…
    is there a way to solve this?

    Reply
    • Jeff Johnson / September 3, 2013

      First and foremost – sorry it took so long for me to respond to your question. We had some problems with our site over the weekend which just recently got resolved.

      Now, let’s move on to your question. To resolve your icon moving issue, try changing this:

      rowBuilder.link.appendTo($(‘tr:last td:last’));

      to this:

      rowBuilder.link.clone().appendTo($(‘tr:last td:last’));

      Calling clone() on the rowBuilder’s link property will give you a copy of the link that represents the “Remove” icon. This way, each row has its own copy of the “Remove” icon/link. Without calling clone() on the link property, the rowBuilder will work directly against the rowBuilder.link. If we’re working against the rowBuilder.link property directly, then we’ll see the odd behavior of the “Remove” icon always being assigned to the last row.

      EDIT: Whoops – looks like the code in Part 3 of the article was wrong! The listing for app/assets/javascripts/ships.js was missing the call to clone(). Sorry about that! I’ve updated the article to show the correct javascript.

      Reply
  • B van Leeuwen / December 29, 2013

    Some things can be realized much more simple than shown in this case.
    The whole popup takes far too much code. The javascript is not unubstrusive, and attaching the whole form build-up code to a submit button is much to error-prone.
    I use Rails 4.0 and found out that using new.js.erb and create.js.erb solve the whole popup problem.
    If you are interested I can tell you how I solved this.

    Reply
    • Jeff Johnson / December 30, 2013

      You’re absolutely right that the javascript isn’t unobtrusive. There are certainly several ways to solve the modal window problem that might be cleaner/simpler than what I’ve talked about here. If you can, would you mind posting a link to your solution so folks can take a look at it?

      Reply

Leave a Comment

Current day month ye@r *


9 × two =