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 4: Switch to Targeting Computer!

4.00/5 (1 vote)
26 Feb 2013CPOL7 min read 12.7K  
We'll take a look at adding some validation rules to our Ship and Pilot models.

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 Shipmust 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:

Save Ship error message

Not too shabby. Coincidentally, we'll get the same error message if we:

  1. Click the "Add Ship" button on the home page to create a new Ship.
  2. Click the "Add Pilot" button on the "New Ship" page to... y'know... Add a new Pilot. And stuff.
  3. Click the "Add" button on the "Add a Pilot" modal form.
  4. 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:

// Set up the UI/UX for the ships screens.  This object sets up all the functionality we need to:
//  1.  Bind to the "click" event of the "#addButton" on the modal form.
//  2.  Append data from the modal form to the Pilots table.
//  3.  Hide the modal form when the user is done entering data.
//
// If any other events need to be wired up, the init function would be the place to put them.
var pilotFieldsUI = {
    init: function() {
        // Configuration for the jQuery validator plugin:
        // Set the error messages to appear under the element that has the error.  By default, the
        // errors appear in the all-too-familiar bulleted-list.
        // Other configuration options can be seen here:  https://github.com/victorjonsson/jQuery-Form-Validator
        var validationSettings = {
            errorMessagePosition : 'element'
        };

        // Run validation on an input element when it loses focus.
        $('#new-pilot-fields').validateOnBlur();

        $('#addButton').on('click', function(e) {
            // If the form validation on our Pilots modal "form" fails, stop everything and prompt the user
            // to fix the issues.
            var isValid = $('#new-pilot-fields').validate(false, validationSettings);
            if(!isValid) {
                e.stopPropagation();

                return false;
            }

            // This is the code from previous posts...
            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:

Pilot modal form with client-side validation in effect.

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:

Validation on the Pilot fields works after adding to the Pilots table.

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?

Pilot validation still happens on the server-side.

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:


License

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