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 <a href="http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for" data-mce-href="http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for">fields_for</a>
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 <a href="https://github.com/jeffjohnson9046/nested-form/blob/master/app/views/ships/_pilot_fields.html.erb" data-mce-href="https://github.com/jeffjohnson9046/nested-form/blob/master/app/views/ships/_pilot_fields.html.erb">_pilot_fields.html.erb</a>
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):
<label for="ship_pilots_attributes_new_pilots_first_name" class="control-label">First name</label>
<input type="text" size="30" name="ship[pilots_attributes][new_pilots][first_name]" class="input-xlarge field" id="ship_pilots_attributes_new_pilots_first_name" />
<label for="ship_pilots_attributes_new_pilots_last_name" class="control-label">Last name</label>
<input type="text" size="30" name="ship[pilots_attributes][new_pilots][last_name]" class="input-xlarge field" id="ship_pilots_attributes_new_pilots_last_name" />
<label for="ship_pilots_attributes_new_pilots_call_sign" class="control-label">Call sign</label>
<input type="text" size="30" name="ship[pilots_attributes][new_pilots][call_sign]" class="input-xlarge field" id="ship_pilots_attributes_new_pilots_call_sign" />
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:
- 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). - Find the closest element that has a
class="fields"
(in our case, this will be a <TR>
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:
- Generate an arbitrary unique id based on the current time.
- Build a regular expression that will search for "new_" + [whatever the name of our
association
is] ("new_pilots" in our case). - Search through our
content
(the string that represents our Pilot
form) and replace "new_pilots" with the unique id we generated in Step 1. - Add the
Pilot
form 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).
- Display the
Pilot
form 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!