Ajax, tabs and Rails: Part 2, 14 March 2009

It has been so long since I wrote part 1 to this tutorial, I've since lost the demo app I was creating. I had to start again, and the views and controller code are different. Best if you get the accompanying code from GitHub and review the controller/view code directly before continuing with this second part.

git://github.com/osahyoun/ajaxed-tabbed-navigation-using-rails.git

To recap, In Part 1 we created a simple controller with some custom routes for displaying a bunch of related content pages. In our views with created a simple tabbed navigation.

Our Enhanced User-Experience will be slapped over the current basic common or garden version of our site. We don't want to ignore those without JavaScript enabled, nor do we want to write JavaScript directly into the view layer/html (messy, hard-to-maintain and not very portable). Never mix your drinks.

For JavaScript assistance I'll be using the very fine Prototype JavaScript library.

Working in application.js we'll create a class for handling the Ajax behaviour. Let it be called Tabs:


var Tabs = Class.create({ 
});

To start, I'll set an event listener on the tabbed menu (a basic ul list of links), so we can catch those mouse clicks when they occur:


var Tabs = Class.create({ 
 initialize: function(){
   $$('ul#nav').first().observe('click', (
     function(event) { 
       if (event.element().tagName == 'A')
          event.stop(); 
          alert(event.element());
       }
     }).bind(this) );	
    }
});

// In other words: Fetch ($$()) from the dom all ul elements with an id of 'nav', and pluck the first() from the array. 
// Now observe() the element for any clicks.
// If you detect a click, hand me some click info as 'event'. 
// If the element() that created the 'event' has the same tagName as a link then stop() the link from successfully making an HTTP request, and alert() us to the element() which caused the event in the first place.	

Before we can check if our listener works, we need to instantiate an instance of the Tabs class. We should do this when the web page has loaded. Prototype makes this easy with:


  // Add this to the bottom of application.js
  document.observe('dom:loaded', function () { new Tabs(); });
  // In other words: Observe the dom, and create a new instance of Tabs() when the dom has loaded.

Run this in your browser and click on a tab. I hope you get an alert window, containing the link you clicked on.

Moving on. Next we'll create a method which writes to the dom the class name 'active' to the parent element of any clicked link (it should be an li element). We'll call this method whenever a link is clicked:


  initialize: function(){		
    $$('ul#nav').first().observe('click', (
      function(event) { 
        if (event.element().tagName == 'A') {
          event.stop(); 
          var element = event.element();

          this.make_link_active(element);
        }
      }).bind(this)
    )
  },

  make_link_active: function(link) {
    $$('ul#nav li').invoke('removeClassName', 'active');
    link.up(0).addClassName('active');
  }
  // In other words: Please fetch all li elements, children to ul#nav.
  // With each li element remove the class 'active', if it exists.
  // When you're done, traverse one element up(0) from the link I gave to you and addClassName('active') to it. 

Save and run. Did it work? I hope yes, because now for the Ajax. Prototype’s Ajax support is exceptional, and has lots to offer. We're going to use just one method offered by the Ajax class (Ajax.Updater)


  fetch_content: function(url) {
    new Ajax.Updater('content', url, {
      method: 'get',
      onLoading: function() { $('spinner').show(); },
      onComplete: function(){ $('spinner').hide(); }
    });
  }
  // In other words: Ajax Update div#content with whatever content the server sends in answer to your request.
 //  Send your request to the url I gave to you. 
 //  onLoading show() us a spinner.
 //  onComplete hide() the spinner. 

Behind the scenes, there's a lot going on. It shouldn't be this easy, but it is. Thank-you Prototype.

Notice the spinner graphic. I've inserted it directly into the html with 'display:none' embedded into the element. For custom spinners visit ajaxload.

Run our new code, click on a tab... whoops!... notice the server is sending back view content plus layout! Let's fix that. In the mammals controller we'll determine whether to render a layout with the view depending on the request type.


class MammalsController < ApplicationController
  layout :determined_by_response
  ............
  
  protected 
  def determined_by_response
    if request.xhr?
      return false
    else
      'application'
    end
  end

That should be all. Our Tabs class is small, with only three methods, but it does the job. The tabbed navigation works with and without JavaScript. Our JavaScript is tucked away in its own file, far away from the html, where it remains easily maintainable for any future improvements/functionality we may want to add. You can also share your Tabs class with friends and family, who need only include the file into their HTML pages for Ajax goodness on their tabbed lists (so long as their menu is a list with an id 'nav', and their server can respond appropriately to the Ajax requests.

Perhaps in a future iteration, we can pass parameters to the class allowing it to deal with different types of menu structure.

Tested only on FF.

blog comments powered by Disqus

AHOY THERE!

We're an Agile web development duo operating from a tiny dinghy currently moored off France. We build accessible, standards driven web solutions using the power of the Ruby on Rails web development framework and efficiency of agile processes... more»

RECENT PROJECTS

RECENT POSTS