AJAX Tabs (Rails redux)

This blog is filled with Rails tidbits, presentations, tutorials, and other web related mumbo-jumbo. The author is Flinn Mueller a veteran web programmer.

Flinn is a proud husband and father, an out doors enthusiast, a foodie, and occasionally allows pictures of his bad yoga form.

Site5 $5 Hosting Deal

Purpose

The purpose of doing Tabs in Ajax is not just to do something in Ajax. The context for which this is used is not for novelty. In this demo we are using Ajax to load information that does not need to be retrieved every time the user pulls the record. The methods used here are to yield absolute control over where the content comes from, what conditions trigger action, and how the panes are presented. This is really meant to be used in an application because the javascript and css are heavy if you are only using them on one page. However if you consider them groundwork for using tabs throughout a larger application then they are pretty light compared to something like YUI.

Background

About a year ago I wrote a tutorial on how to use prototype, plain old html, and css to create AJAX Tabs. The tutorial was a plug for a PHP based Rails clone I was working on called Biscuit (no I didn't name it that, someone else did). Anyhow Biscuit is now defunct (Long live Rails), but the tutorial still works just fine in Rails. So read on...

Concept

The concept here is simple, I have a Part Vendor, which consists of a `vendor` model with a has_many :through associaton to `parts`, and polymorphic association to `notes`. I want to display the Vendor info in the first tab, and have a long list of parts in the second tab, a map of the vendor location and any related notes, however since Parts and Notes can be extensive I will not preload these. I don't want to reload Parts or Notes every time I click their respective tabs either. So when you click the tabs for parts and notes they load into their containers, but when you click them again the innerHTML is already set, so no need to grab the data again.

The Results

  • Company Right Traq, Inc.

    Street 7 S. Main St. Suite 225
    City Wilkes-Barre
    State PA
    Zip Code 18701

    Contact Flinn Mueller Title President
    Email info.righttraq.com

    Phone (570) 408-9825 Alternate
    Fax Alternate
    Mobile Text
    Pager Skype

    Website http://www.righttraq.com

See How It's Done

The AJAX Tabs

<%= javascript_include_tag("http://api.maps.yahoo.com/v3.0/fl/javascript/apiloader.js?appid=yourownkey") %>

<ul class="tabselector" id="tabcontrol1">
  <li class="tab-selected" id="vendor_tab">
    <%= link_to_function('Vendor', "tabselect($('vendor_tab')); paneselect($('vendor_pane'))") %></li>
  <li class="tab-unselected" id="part_tab">
    <%= link_to_function('Parts', "loadPane($('part_pane'), '" + url_for(:action => 'parts', :id => @vendor) + "'), tabselect($('part_tab')); paneselect($('part_pane'))") %></li>
  <li class="tab-unselected" id="map_tab">
    <%= link_to_function('Map', "loadPane($('map_pane'), '" + url_for(:action => 'map', :id => @vendor) + "'), tabselect($('map_tab')); paneselect($('map_pane'))") %></li>
  <li class="tab-unselected" id="notes_tab">
    <%= link_to_function('Notes', "loadPane($('notes_pane'), '" + url_for(:controller => 'notes', :action => 'list', :notable_id => @vendor, :notable_type => @vendor.class) + "'), tabselect($('notes_tab')); paneselect($('notes_pane'))") %></li>
</ul>

<ul class="panes" id="panecontrol1">
  <li id="vendor_pane" class="pane-selected">
    <%= render :partial => 'show' %>
  </li>
  <li id="part_pane" class="pane-unselected"></li>
  <li id="map_pane" class="pane-unselected">
    <%= render :partial => 'map' %>
  </li>
  <li id="notes_pane" class="pane-unselected"></li>
<ul>

My Javascript

function tabselect(tab) {
  var tablist = $('tabcontrol1').getElementsByTagName('li');
  var nodes = $A(tablist);
  var lClassType = tab.className.substring(0, tab.className.indexOf('-') );

  nodes.each(function(node){
    if (node.id == tab.id) {
      tab.className=lClassType+'-selected';
    } else {
      node.className=lClassType+'-unselected';
    };
  });
}

function paneselect(pane) {
  var panelist = $('panecontrol1').getElementsByTagName('li');
  var nodes = $A(panelist);

  nodes.each(function(node){
    if (node.id == pane.id) {
      pane.className='pane-selected';
    } else {
      node.className='pane-unselected';
    };
  });
}

function loadPane(pane, src) {
  if (pane.innerHTML=='' || pane.innerHTML=='<img alt="Wait" src="/images/spinner.gif" style="vertical-align:-3px" /> Loading...') {
    reloadPane(pane, src);
  }
}

function reloadPane(pane, src) {
  new Ajax.Updater(pane, src, {asynchronous:1, evalScripts:true, onLoading:function(request){pane.innerHTML='<img alt="Wait" src="/images/spinner.gif" style="vertical-align:-3px" /> Loading...'}})
}

My Fancy CSS

.tabselector, .tab-selector {
  width: auto;
  border-bottom: 1px solid #c0c0c0;
  padding: 10px 0 0 20px;
}

.tab-unselected {
  display: inline;
  padding: 2px 7px 0 7px;
  background: #f0f0f0;
  border: 1px solid #c0c0c0;
  border-bottom: 0;
  color: #c0c0c0;
}

.tab-selected {
  display: inline;
  padding: 3px 7px 1px 7px;
  background: #ffffff;
  border: 1px solid #c0c0c0;
  border-bottom: 0;
}

.tab-unselected a {
  padding: 6px;
  color: #a0a0a0;
}

.tab-selected a {
  font-weight: bold;
  color: #0066CC;
  padding: 6px;
}

.panes, .pane-selector {
  width: 97%;
  padding-left: 0px;
  margin: 2%;
  min-height: 300px;
  overflow: auto;
}

.pane-selected {
  list-style-type: none;
  display: block;
  padding: 10px;
}

.pane-unselected {
  list-style-type: none;
  display: none;
}