Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Ruby

Rails 3.2: A Nested-Form Demo, Part 3: We're Starting Our Attack Run!

4.00/5 (1 vote)
11 Feb 2013CPOL7 min read 19.1K  
Rails 3.2: A Nested-Form Demo, Part 3: We're Starting Our Attack Run!

Overview

When we last left our heroes, they fought their way through the superstructure of our application and were setting up to finish it off. Having defeated the TIE fighter screen, our intrepid fighter pilots have descended into the trench of the Starfighter Recognition Guide. Their computers are locked, they're getting a signal...

In this post, we'll capitalize on the code and plumbing we implemented here and here. We'll take a look at the views we put in place and some JavaScript to make our Starfighter Recognition Guide a bit fancy-pants.

Stabilize your rear deflectors and watch for enemy fighters - there's going to be a tubolaser battery's worth of code in this one.

The Index View

There isn't a whole lot of interesting stuff going on in the home/landing page of the Starfighter Recognition Guide, (i.e. our index.html.erb). It's basically a table that shows all of the Ships that have been entered so far. Here's a pretty picture of what the home page looks like:

Home page of the Starfighter Recognition Guide

I know - neat-o, right?  If you want to see the source code for the index.html.erb view, you can check it out here.

The _form Partial View

Because functionality for adding a new Ship or editing an existing Ship is largely the same, I've created a _form.html.erb partial view that encapsulates all the data entry fields and extra goodies that we'll need to persist our Ships and Pilots. Here's what it looks like:

app/views/ships/_form.html.erb:

XML
<%= form_for(@ship, :html => {:class => "form form-horizontal"}) do |f| %>
    <fieldset>
      <legend><%= "#{ @ship.name } " unless @ship.name.nil?  %>Ship Information</legend>
      <%= render('error_messages', :object => f.object) %>
      <div class="control-group">
        <%= f.label(:name, :class => "control-label") %>
        <div class="controls">
          <%= f.text_field(:name, :class => "input-xlarge") %>
        </div>
      </div>

      <div class="control-group">
        <%= f.label(:crew, :class => "control-label") %>
        <div class="controls">
          <%= f.text_field(:crew, :class => "input-xlarge") %>
        </div>
      </div>

      <div class="control-group">
        <%= f.label(:has_astromech, :class => "control-label") %>
        <div class="controls">
          <%= f.check_box(:has_astromech, :class => "checkbox") %>
        </div>
      </div>

      <div class="control-group">
        <%= f.label(:speed, :class => "control-label") %>
        <div class="controls">
          <%= f.text_field(:speed, :class => "input-xlarge") %>
        </div>
      </div>

      <div class="control-group">
        <%= f.label(:armament, :class => "control-label") %>
        <div class="controls">
          <%= f.text_area(:armament, :class => "input-xlarge", :rows => "3") %>
        </div>
      </div>
    </fieldset>

    <fieldset>
      <legend>Pilots <%= "That Fly the  #{ @ship.name }" unless @ship.name.nil? %></legend>
      <%= render('pilots_table', :f => f) %>
      <%= link_to_add_fields("Add a Pilot", f, :pilots, :class => "btn btn-primary", :title => "Add a new Pilot to the list of Pilots that fly this Ship.") %>
      <p></p>
    </fieldset>

    <div class="modal-footer">
      <%= f.submit("Save", :class => "btn btn-primary", :title => "Save the changes to this Ship.") %>
      <%= link_to("Cancel", ships_path, :confirm => "Are you sure you want to cancel?  Any changes will be lost.", :class => "btn btn-inverse", :title => "Cancel the changes and return to the Home page.") %>
    </div>
<% end %>

The interesting part is on line 43, where we're rendering the _pilots_table.html.erb partial view. Notice how we're passing our form to it? That'll allow us to use the fields_for method on the "parent" form (i.e. the Ship's form) to generate input fields for all the Ship's Pilots. Let's take a look at the _pilots_table.html.erb partial view, shall we? Yes, let's:

app/views/ships/_pilots_table.html.erb:

XML
<table id="pilots-table" class="table table-hover table-striped">
  <thead>
    <th>First Name</th>
    <th>Last Name</th>
    <th>Call Sign</th>
  <th></th>
  </thead>
  <tbody>
    <%= f.fields_for(:pilots) do |pilots_form| %>
      <tr class="fields">
        <td><%= pilots_form.text_field(:first_name) %></td>
        <td><%= pilots_form.text_field(:last_name) %></td>
        <td><%= pilots_form.text_field(:call_sign) %></td>
        <% if current_page?(new_ship_path) || current_page?(edit_ship_path) %>
            <td>
              <%= link_to_remove_fields('<i class="icon-remove"></i>'.html_safe, pilots_form, :title => "Delete this Pilot.") %>
            </td>
        <% end %>
      </tr>
    <% end %>
  </tbody>
</table>

On line 9, we're calling fields_for to generate the input fields for each Pilot associated to our Ship. Also notice on line 16 we're using our link_to_remove_fields helper method (described in exhaustive detail here) to create a link that will allow us to delete the Pilot if we want. Because we've used fields_for to wire up the input fields for our associated Pilots, we can edit any of the Pilots and/or the Ship's attributes. When we submit our form, everything will be saved to the database in one swell foop. To see what the POST might look like when this form is submitted, you can take a look here.

To see this in action, let's take a look at a screenshot of a Ship being edited:

Editing a Ship with Pilots

Adding a new Ship and Pilot

As mentioned previously, adding a new Ship is pretty much like editing an existing Ship. Let's add a new ship to our Starfighter Recognition Guide: The TIE Interceptor. We click the "Add a Ship" button on the home/landing page, and end up with something like this:

Add the Tie Interceptor

Now we click on the "Add a Pilot" button, which renders our _pilot_fields.html.erb partial view as a modal form. We talked about the mechanism to make this work (our link_to_add_fields helper method) in the last post. Shown below is a screenshot of the modal form that allows us to add a new Pilot:

 Add a Pilot to the Tie Interceptor with a modal form

Here's what the _pilot_fields.html.erb looks like under the covers:

app/views/ships/_pilot_fields.html.erb:

XML
<div id="new-pilot-fields" class="modal fade" data-backdrop="static">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
    <h3>Add a Pilot<%= " to the #{ @ship.name } Pilot Roster" unless @ship.name.nil? %></h3>
  </div>
  <div class="modal-body">
    <fieldset>
      <div class="control-group">
        <%= f.label(:first_name, :class => "control-label") %>
        <div class="controls">
          <%= f.text_field(:first_name, :class => "input-xlarge field") %>
        </div>
      </div>

      <div class="control-group">
        <%= f.label(:last_name, :class => "control-label") %>
        <div class="controls">
          <%= f.text_field(:last_name, :class => "input-xlarge field") %>
        </div>
      </div>

      <div class="control-group">
        <%= f.label(:call_sign, :class => "control-label") %>
        <div class="controls">
          <%= f.text_field(:call_sign, :class => "input-xlarge field") %>
        </div>
      </div>
      <%= f.hidden_field(:_destroy, :class => "field") %>
    </fieldset>
  </div>
  <div class="modal-footer">
    <button id="addButton" type="button" class="btn btn-primary" data-dismiss="modal" aria-hidden="true" title="Add this Pilot to the list of Pilots that are assigned to this Ship.">Add</button>
    <button id="cancelButton" type="button" class="btn btn-inverse" data-dismiss="modal" aria-hidden="true" title="Close this screen without adding the Pilot to the list.">Cancel</button>
  </div>
</div>

<script type="text/javascript">
  pilotFieldsUI.init();
</script>

There isn't anything very fancy going on in the _pilot_fields.html.erb partial view. I'd go so far as to say the only interesting bit is the script tag at the bottom. What's going on there? Let's find out!

JavaScript for the Pilot Modal Form

So, we have our Pilot input fields appearing in a modal form. What now? What should happen when a user clicks the "Add" button on the modal form? How will a user be able to add multiple Pilots to a single Ship?

Well, it'd be nice if the newly created Pilot was added to the "Pilots" table shown in the "Add a Ship..." screen (shown above). Ideally, it'd be nice if the rows looked exactly the same as the "Pilots" table in the "Editing..." screen (also shown above).

app/assets/javascripts/ships.js:

var pilotFieldsUI = {
    init: function() {
        $('#addButton').on('click', function() {
            formHandler.appendFields();
            formHandler.hideForm();
        });
    }
};

The code above shows our pilotFieldsUI object literal, which has one function: init. The init function wires up the addButton on our Pilot modal form. As you can see, a couple of methods will be invoked when a user clicks the "Add" button on the Pilot modal form. Let's check those out next:

app/assets/javascripts/ships.js:

var formHandler = {
    // Public method for adding a new row to the table.
    appendFields: function() {
        // Get a handle on all the input fields in the form and detach them from the DOM (we will attach them later).
        var inputFields = $(cfg.formId + ' ' + cfg.inputFieldClassSelector);
        inputFields.detach();

        // Build the row and add it to the end of the table.
        rowBuilder.addRow(cfg.getTBodySelector(), inputFields);

        // Add the "Remove" link to the last cell.
        rowBuilder.link.appendTo($('tr:last td:last'));
    },

    // Public method for hiding the data entry fields.
    hideForm: function() {
        $(cfg.formId).modal('hide');
    }
};

Again, the formHandler is nothing spectacular. The idea behind the formHandler is like this: the formHandler manages the orchestration of all the fancy UI stuff. He'll delegate the specifics (e.g. building a row element w/the Pilot input fields) to other objects. In this case, we want to build a row that we'll add to the Pilots table and we'll want to hide the modal form when we're done. To achieve that, we have a couple of methods:

  • appendFields: This method will add the input fields from the Pilot modal form and add them as a new row to the Pilots table on the Ship form.
  • hideForm: Hides/dismisses the Pilot modal form (thanks, Twitter Bootstrap!).

I wanted to keep all of my IDs and other "magic string" type stuff in one place, so I created a quick cfg object literal that looks like what's shown below:

app/assets/javascripts/ships.js:

var cfg = {
    formId: '#new-pilot-fields',
    tableId: '#pilots-table',
    inputFieldClassSelector: '.field',
    getTBodySelector: function() {
        return this.tableId + ' tbody';
    }
};

The cfg object is referenced by the formHandler and the rowBuilder. That gives me an excellent segue to the rowBuilder, which looks like this:

app/assets/javascripts/shipsjs:

var rowBuilder = function() {
    // Private property that define the default <TR> element text.
    var row = $('<tr>', { class: 'fields' });

    // Public property that describes the "Remove" link.
    var link = $('<a>', {
        href: '#',
        onclick: 'remove_fields(this); return false;',
        title: 'Delete this Pilot.'
    }).append($('<i>', { class: 'icon-remove' }));

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

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

        return newRow;
    }

    // A public method for building a row and attaching it to the end of a <TBODY> element.
    var attachRow = function(tableBody, fields) {
        var row = buildRow(fields);
        $(row).appendTo($(tableBody));
    }

    // Only expose public methods/properties.
    return {
        addRow: attachRow,
        link: link
    }
}();

The rowBuilder has one purpose in life - to build a <TR> element that will be added to the "Pilots" table. The <TR> element will contain the input fields from the Pilot modal form.

The rowBuilder is a bit more complex than his brethren cfg or formHandler. There are some properties/methods of the rowBuilder that don't need to be called by anyone else, so I wanted to keep those private. To achieve that, I used the revealing module pattern (sounds awesome, right?!) to only expose the methods that should be... err... exposed, I guess. I like this pattern a lot and I think you get a lot of bang for your buck by using it. It's easy to implement, makes code more readable, and clearly communicates the intent to other developers. With an extra line or two, we've told the next guy/girl who works on this project what parts of the rowBuilder are guts/plumbing and what parts are intended for use by other components of our program.

Code smell: The rowBuilder.link property should probably be private and should probably be added to the new <TR> element before it's returned. Right now, the rowBuilder.link property is used in the formHandler.appendFields() method (line 12 in the formHandler snippet). This is an area that is ripe for refactoring.

With all that javascript, what happens when a user clicks the "Add" button after adding a new Pilot to the Tie Interceptor Ship? Well, the screen looks like this:

Add a Ship with a Pilot

Summary: It's Away!

So... there you have it. We created a new TIE Interceptor Ship with a Pilot named Maarek Stele. Because of our implementation, the user can change any of the Pilot attributes before committing everything to the database. When the user clicks the "Save" button, the POST will contain the Ship and Pilot attributes (again, en example of what the POST will look like can be found here).

Devil's Advocate: My Scope's Negative, I Don't See Anything!

Hey Jeff, how come you didn't just append a row with new input fields to the 'Pilots' table to begin with? What's with all the modal mumbo jumbo?
Sure, we could've done that. If you remember from a long time ago in a post far far away, the mission was to add "child" objects to a parent object using a modal data entry form. Besides, I think the modal data entry form makes for a better user experience. Adding a new row of data entry fields would be a little subtle, don'cha think? If you pop up a modal form in front of the user, there's no confusion about what's going on. It's very clear (I hope, anyway) that the user is supposed to provide data for a Pilot and add it to the list.

Reference

  • Railscast 197 was instrumental in getting me started with accepts_nested_attributes_for and fields_for.
  • The source code for the Starfighter Recognition Guide can be found here

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)