Overview
Our heroes have fought their way through the Death
Star's fighter screen and can see the trench. As they enter the trench
our pilots stabilize their deflector shields, preparing for the torrent
of incoming turbolaser fire that will no doubt be coming their way.
In this post, we'll take a look at adding some validation rules to our Ship
and Pilot
models. Hopefully, this effort will prevent our users from... err...
having their way with our meek and rather naive Starfighter Recognition
Guide. Our exhaust port is small, it'd be a shame to see it damaged by
some ham-fisted users.
Validation
In our last post we walked away with a fully-functioning Starfighter Recognition Guide that could save a Ship
and any changes (create/update/delete) to its associated Pilots
. Neat, right?
Before
we run off and save everything willy-nilly, we should probably go about
checking to make sure our users provided the data we need. Let's add
some data entry requirements to our application, shall we? Here's what
we need to do:
Ship Requirements
- A
Ship
must have a name. - A
Ship
's name must be between 3 and 50 characters. - A
Ship
must have a crew. - A
Ship
must have between 1 and 5 crew members.- Arbitrary? Sure. But I figure if a
Ship
has more than 5 crew members, it's probably a capital ship or on a
scale that isn't appropriate for a Starfighter Recognition Guide.
- A
Ship
must have a speed. - A
Ship
's speed must be between 50 and 200... err... thingies per... umm... unit of time.- At
the risk of having my Star Wars nerd card revoked, I'm not sure how the
speed of a starfighter is measured. Yes, I looked it up. Nope, I
couldn't find anything conclusive.
Shown below is the complete Ship
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 => :all_blank,
:allow_destroy => true
validates :name,
:presence => true,
:uniqueness => { :case_sensitive => false },
:length => { :maximum => 50, :minimum => 3 }
validates :crew,
:presence => true,
:inclusion => { :in => 1..5, :message => "must be between 1 and 5" }
validates :speed,
:presence => true,
:inclusion => { :in => 50..200, :message => "must be between 50 and 200" }
end
REFACTOR: The code above has changed a bit from the first post. Specifically, I had to change the :reject_if
from lambda { |attrs| attrs.all? { |key, value| value.blank? }
to :all_blank
. After reading up on the documentation for <a data-mce-href="http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for" href="http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for">accepts_nested_attributes_for</a>
, the Proc
I was using wasn't good enough. Why, you ask? Well, our Pilot
objects have their special _destroy
field attached to them. The _destroy
attribute can only be true
or false
, so it won't ever be blank. The Proc
above would allow us to save "empty" Pilot
records (i.e. Pilots
that have null
first_name
, last_name
and call_sign
values). The :all_blank
symbol points to an ActiveRecord Proc
that deals with this specific issue. Using :reject_if => :all_blank
means that any Pilot
record with no data will be ignored during the save, which is what we want. So... yay.
Pilot Requirements
- A
Pilot
must have a first name. - A
Pilot
's first name cannot be longer than 50 characters. - A
Pilot
must have a last name. - A
Pilot
's last name cannot be longer than 50 characters. - A
Pilot
must have a call sign. - A
Pilot
's
call sign must be unique. Case-sensitivity doesn't matter here - "red
5" is the same as "Red 5", which is the same as "RED 5", etc.- To make sure this requirement is enforced, a unique index has been added to the
call_sign
column of the pilots
table. You can see it on line 13 of the database migration script here.
Here's what the Pilot
model looks like with the validation in place:
app/models/pilot.rb
class Pilot < ActiveRecord::Base belongs_to :ship attr_accessible :call_sign, :first_name, :last_name, :ship_id validates :first_name, :presence => true,
:length => { :maximum => 50 }
validates :last_name,
:presence => true,
:length => { :maximum => 50 }
validates :call_sign,
:presence => true,
:uniqueness => { :case_sensitive => false },
:length => { :maximum => 50, :minimum => 5 }
end
Okey doke - now that we have our validation in place, let's
see what happens when we click the "Add Ship" button and then click
"Save" without entering anything:
Not too shabby. Coincidentally, we'll get the same error message if we:
- Click the "Add Ship" button on the home page to create a new
Ship
. - Click the "Add Pilot" button on the "New Ship" page to... y'know... Add a new
Pilot
. And stuff. - Click the "Add" button on the "Add a Pilot" modal form.
- We'll see a new row added to the "Pilots" section of the "Add Ship" form, and all the fields will be empty.
How come we don't get any errors about the Pilot
with empty fields? That's our Ship
's :reject_if
in action. ActiveRecord saw that the Pilot
was "empty", so it discarded the record.
Allowing
a user to click the "Add" button on our "Add a Pilot" modal form with
willful and blatant disregard for our carefully-laid requirements is an
affront to our humble application. It will not stand, I tell you! 'Kay -
maybe that was a little harsh, but you know what I mean. If we don't
stand up for our app, who will? If we need the user to give us data for a
Pilot
, we should do what we can to make sure they give it
to us. Let's add some client-side validation to our "Add a Pilot" modal
form, so we can tell the user what's missing when they click the "Add"
button.
I poked around on Google for a bit to find a good solution
for client-side validation utilities, and there's no shortage of them. I
landed on this article, which uses the jQuery-Form-Validator. The implementation looks pretty straightforward, and +1 for the article because he's using Twitter Bootstrap too.
The jQuery-Form-Validator
Adding the jQuery-Form-Validator is pretty straightforward. Just get the jquery-form-validator.min.js
file to your project. There aren't any dependencies (outside of jQuery, of course).
Using the jQuery-Form-Validator is also pretty simple. All you have to do is add a data-validation
attribute to the element
you want to validate. You can visit the GitHub page to see all the validation options that are available (there are quite a few).
You can also customize the error message, by adding a data-validation-error-msg
attribute with your customized error message. There are a host of other
configuration options available, such as: the position of the error
messages, the name of the data-*
attribute, the CSS classes for your error messages, etc. You can see them on the GitHub page or on the jQuery-Form-Validator demo page.
Shown below is the code for our Pilot
modal form that implements the jQuery-Form-Validator:
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", :data => { :validation => "required validate_max_length length50", "validation-error-msg" => "First Name is required and cannot be longer than 50 characters." }) %>
</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", :data => { :validation => "required validate_max_length length50", "validation-error-msg" => "Last Name is required and cannot be longer than 50 characters." }) %>
</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", :data => { :validation => "required validate_length length5-50", "validation-error-msg" => "Call Sign is required and cannot be must be between 5 and 50 characters." }) %>
</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>
Just like this fine fellow did, we'll add some CSS to our application.css file to make sure our error messages appear below the offending element:
app/assets/stylesheets/application.css.scss:
.jquery_form_error_message{
display: block;
margin-top: 3px;
color: #f00;
}
Now all we have to do is wire up the jQuery-Form-Validator to our "Add a Pilot" modal form. Here's how we do that:
app/assets/javascripts/ships.js:
var pilotFieldsUI = {
init: function() {
var validationSettings = {
errorMessagePosition : 'element'
};
$('#new-pilot-fields').validateOnBlur();
$('#addButton').on('click', function(e) {
var isValid = $('#new-pilot-fields').validate(false, validationSettings);
if(!isValid) {
e.stopPropagation();
return false;
}
formHandler.appendFields();
formHandler.hideForm();
});
}
};
On line 13, we're building a configuration object that
contains our setting for the jQuery-Form-Validator. All we want to do is
set the error messages to appear next to the input element
,
so that's all we're setting for now. On line 18, we're telling the
jQuery-Form-Validator to fire whenever one of our elements loses focus.
Am I over-explaining? Probably. Almost certainly. But it's too late now!
On line 23, things get slightly more interesting. Since the "Add a Pilot" modal form isn't a proper form (remember, it's a <div>
on our Ship form
), our "Add" button isn't a submit
. However, we're going to call .validate
on our #new-pilot-fields
anyway. If our validation fails, then stop everything and let the user fix their issues.
Now that we've wired everything up, here's what it looks like after a user click's the "Add" button:
As an added bonus, the validation functionality is carried forward when we add the new Pilot
to our "Pilots" section of the Ship form
. Here's what it looks like when a user adds a Pilot
successfully, but then removes the Call Sign before clicking save:
Caveat
The trick shown above isn't 100% bullet-proof - remember we aren't calling jQuery-Form-Validator's validate()
method when we submit Ship form
. It'd be pretty easy to implement, so I'll leave that as an exercise for you, dear reader. However, we are covered by our server-side validation that we implemented in our models. See?
It's not perfect, but it's better than nothing. We'll try to clean this up in a later post.
Conclusion
We're definitely
closing in on the exhaust port now, ladies and gentelmen! Our
Starfighter Recognition Guide's shields have been hardened with some
client-side validation. These shield upgrades will come in handy to
deflect the incoming fire our users will no doubt throw at us. Our
intrepid heroes aren't quite out of the woods yet - there's some
additional validation we could add (isn't there always?), but we'll
handle that as our pilots approach their target.
Reference
Here are some articles/tools that helped me out immensely with this post: