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:
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:
<%= 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:
<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:
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:
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
:
Here's what the _pilot_fields.html.erb looks like under the covers:
app/views/ships/_pilot_fields.html.erb:
<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 = {
appendFields: function() {
var inputFields = $(cfg.formId + ' ' + cfg.inputFieldClassSelector);
inputFields.detach();
rowBuilder.addRow(cfg.getTBodySelector(), inputFields);
rowBuilder.link.appendTo($('tr:last td:last'));
},
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() {
var row = $('<tr>', { class: 'fields' });
var link = $('<a>', {
href: '#',
onclick: 'remove_fields(this); return false;',
title: 'Delete this Pilot.'
}).append($('<i>', { class: 'icon-remove' }));
var buildRow = function(fields) {
var newRow = row.clone();
$(fields).map(function() {
$(this).removeAttr('class');
return $('<td/>').append($(this));
}).appendTo(newRow);
return newRow;
}
var attachRow = function(tableBody, fields) {
var row = buildRow(fields);
$(row).appendTo($(tableBody));
}
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:
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.