Expert Software Development, Staffing & Consulting


Rails 3.2: A Nested-Form Demo, Part 2: Accelerate to Attack Speed!

Written by Jeff Johnson on Jan 24 2013 - Write the first comment

Overview

In our previous post, our intrepid heroes were hurtling headlong into the trenches of the Starfighter Recognition Guide. They maneuvered through the superstructure of the application, setting up a relationship between themselves (the Pilots) and the Ships they fly. In this post, our ace Rebel pilots will make their approach to the surface of our application.

The Controller

Because all of the heavy lifting is being handled by the ActiveRecord configuration defined in our domain model, the controller for the Ship model is pretty standard fare. As a quick refresher, here’s what the create method in the ships_controller looks like:

  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

If you’re interested in looking at the controller in its entirety, you can check it out here.

The Helper Methods

Taking a cue from the Railscast, I’ve added a couple of helper methods to make my life a little easier:

app/helpers/application_helper.rb:

module ApplicationHelper
  def link_to_remove_fields(name, f, options = {})
    f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)", options)
  end

  def link_to_add_fields(name, f, association, options = {})
    new_object = f.object.class.reflect_on_association(association).klass.new
    fields = f.fields_for(association, new_object, :child_index => "new_#{ association }") do |builder|
      render(association.to_s.singularize + "_fields", :f => builder)
    end

    link_to_function(name, "add_fields(this, "#{ association }", "#{ escape_javascript(fields) }")", options)
  end
end

The code above is lifted pretty much verbatim from the Railscast, but I still think it’s worth going over:

  • link_to_remove_fields: This method will create a hidden _destroy field (which tells us whether or not the record should be deleted) and a hyperlink that will invoke a javascript method to update our _destroy field.
  • link_to_add_fields:This method will:
    • Create a new instance of our association object (a new Pilot in this case).
    • Builds a form that we can use to edit our new Pilot object.
    • The :child_index will be a placeholder that will be replaced by a unique value generated in javascript (more on that in a minute).
    • Build a hyperlink containing a form for our Pilot object.

The fields_for Method

Let’s take a look at what’s going on with the second and third lines of link_to_add_fields (lines 8 and 9 in the code shown above). We’re using the fields_for method to build the Pilot input fields for us. At a high level, the fields_for method allows us to build an HTML form without the <form> tags. That means we can put our fields in the “parent” form without any problems.

The fields_for method takes a few parameters:

  • record_name: The name of the type of record we want to create. In our case, we’ll pass “Pilots” for this parameter.
  • record_object: An instance of the object we want to add/edit. In our case, this will be a Pilot object. We created a new Pilot object on the previous line of our link_to_add_fields method (line 7 in the code shown above).
  • options: Any options that we might want to pass to the fields_for method. In our case, we want a way to uniquely identify each Pilot we create. So, we use a :child_index, and set it to a “placeholder”. For this specific example, our placeholder text will be “new_pilots”. This placeholder will be replaced with a unique identifier when the form is shown (i.e. when the add_fields javascript method is called).

In our fields_for block, we’re asking it to render our _pilot_fields.html.erb partial view. Observe the second parameter, :f => builder is the parent form (i.e. the form that contains the input fields for the Ship we’re working with). This means that the Pilot fields rendered in our call to fields_for will be a part of the “Add a Ship” form, which is exactly what we want.

At this point, our fields variable should look like a bit like this (tidied up so it can actually be read):

<div class="modal fade" id="new-pilot-fields">
<div class="modal-header"><button class="close" type="button">×</button>
<h3>Add a Pilot</h3>
</div>
<div class="modal-body">
<fieldset>
<div class="control-group"><label class="control-label" for="ship_pilots_attributes_new_pilots_first_name">First name</label>
<div class="controls"></div>
</div>
<div class="control-group"><label class="control-label" for="ship_pilots_attributes_new_pilots_last_name">Last name</label>
<div class="controls"></div>
</div>
<div class="control-group"><label class="control-label" for="ship_pilots_attributes_new_pilots_call_sign">Call sign</label>
<div class="controls"></div>
</div></fieldset>
</div>
<div class="modal-footer"><button class="btn btn-primary" id="addButton" title="Add this Pilot to the list of Pilots that are assigned to this Ship." type="button">Add</button> <button class="btn btn-inverse" id="cancelButton" title="Close this screen without adding the Pilot to the list." type="button">Cancel</button></div>
</div>

In the last line of the link_to_add_fields method (line 12 in the code above), we hand off the contents of the fields variable to the link_to_function method. The link_to_function method is setting up a link that will call our add_fields javascript method (described in the next section). The fields value will be used as the content parameter of the add_fields method.

The Javascript

What’s a web app without a little javascript? Our helpers shown above make use of the Rails built-in link_to_function helper. The link_to_function will be used to call the javascript methods shown below.

app/assets/javascripts/application.js:

function remove_fields(link) {
    $(link).prev("input[type=hidden]").val("1");
    $(link).closest(".fields").hide();
}

function add_fields(link, association, content) {
    var new_id = new Date().getTime();
    var regex = new RegExp("new_" + association, "g");
    $(link).parent().after(content.replace(regex, new_id));
    $('#new-pilot-fields').modal('show');
}

Again, the code shown above comes pretty much verbatim from the Railscast. The remove_fields function is pretty straightforward:

  1. Find the hidden input field that comes before our “Remove” (our _destroy field) link and set it’s value to 1 (i.e. true, meaning we want ActiveRecord to delete this record for us).
  2. Find the closest element that has a class="fields" (in our case, this will be a &ltTR> element) and hide it. Presto! As far as the UI is concerned, we’ve deleted a Pilot!

The add_fields function is a little more involved, but there’s nothing too fancy:

  1. Generate an arbitrary unique id based on the current time.
  2. Build a regular expression that will search for “new_” + [whatever the name of our association is] (“new_pilots” in our case).
  3. Search through our content (the string that represents our Pilot form) and replace “new_pilots” with the unique id we generated in Step 1.
  4. Add the Pilotform to the DOM.
    • To be very clear, the HTML shown in the description of the fields_for method will be added to our Add a Ship screen (which we haven’t seen yet, but trust me – it’s on its way).
  5. Display the Pilotform as a modal popup.

Now, when a user clicks our “Add a Pilot” link (which we’ll see in the next article), a modal form with our Pilot input fields will appear. Hooray!

Summary: Cut the Chatter, Red 2!

We’re bouncing through the magnetic field with our deflectors on. While we haven’t started our attack run on the views yet, our approach is set up – the code in this post will make setting up our views a whole lot easier. In our next installment, we’ll switch on our targeting computers and attack the views. Before they know it, our Rebels will have a full-fledged Starfighter Recognition Guide with which to record their exploits and tales of derring-do.

Stay on target!

<< Part 1:  All Wings Report In!   Part 3:  We’re Starting Our Attack Run! >>

Leave a Comment

Current day month ye@r *


four × = 24