ATOM/RSS via jQuery

by: Jeremy Miller

Contents
  1. Desired Features
  2. Required jQuery Components
  3. File Structure
  4. The Code
  5. Constants
  6. Member-Variables
  7. Methods
    1. METHOD: init
    2. METHOD: setViewState
    3. METHOD: showMinimized
    4. METHOD: showMaximized
    5. METHOD: tabLoaded
    6. METHOD: jumpToSelectedFeed
    7. METHOD: xmlReturnFilter

The jQuery library is a great library for easy Ajax and XML scripting. In this article, we'll explore the Ajax and XML scripting features to create an ATOM/RSS reader which displays your webpage's feeds for ease of access and reading without having to leave your site to go to their ATOM/RSS aggregator(s).

Desired Features

Let's begin by looking at the features we'd like our script to have:

  1. Since this feature requires JavaScript, it must not have any observable effect on the end-user's experience when JavaScript is disabled.
  2. Only feeds which are made public through the LINK tag should be automatically-detected.
  3. Multiple feeds should be allowed and displayed as separate tabs using the jQuery UI Tab Widget.
  4. Entries should be listed to allow quick navigation to the user-selected entry.
  5. The visitor needs to be able to easily access and close the display of feeds.

Required jQuery Components

To build our script, it will be helpful to identify which jQuery widgets we'll need (for http://jqueryui.com/download ):

  1. The jQuery Core
  2. The UI Core
  3. The UI Selectable Widget

    We'll use this as navigation to make our list of feeds selectable by the visitor.

  4. The UI Tab Widget
  5. The UI Dialog Tool

    We'll use this to add a close button to the Tab Widget

  6. Cookie management using the plugin available at http://dev.jquery.com/browser/trunk/plugins/cookie/

    We'll use this to remember whether the tab display is visible or hidden from across page views and sessions.

  7. Our chosen theme.

File Structure

Once everything is downloaded and setup on our development page, we'll need just a couple additional files before we're ready to build our plugin:

  1. Our HTML output file. In this case, we'll call it index.html.
  2. Our custom stylesheet file. In this case, we'll call is stylesheet.css.
  3. Our custom javascript file. In this case, we'll call is raj.js

The above setup, with our RSS and ATOM feeds, gives us our basic framework, so now we can move on to the coding…

The Code

Now that we have jQuery setup and our basic file structure, we can begin to build in our custom functionality.

In staying with the principles of object-oriented programming, we'll need to layout our object. To layout our object, we need to define 1) constants, 2) member-variables, and 3) methods.

Constants

For this simple script, we'll really need only 2 constants — one for each state, maximized or minimized, of the script. We'll call these SETTING_MINIMIZED and SETTING_MAXIMIZED.

Member-Variables

There are also only 2 things here we need: a list of feeds and the current view state of the script. Let's call these referenced_feeds and current_settings, respectively.

Methods

Our script will have 5 primary functions, each requiring a method:

  1. Display the view for when the script is minimized. We'll call this method showMinimized.
  2. Display the view for when the script is maximized. We'll call this method showMaximized.
  3. Update feeds when a tab is loaded. We'll call this method tabLoaded.
  4. Allow the user to jump to a selected feed entry. We'll call this method jumpToSelectedFeed.
  5. Show the correct initial state when the page is visited. We'll call this method init.

After a closer look, there are 2 more methods which will provide for a more structured approach:

  1. Since the script will need to be able to adjust which view is shown in multiple scenarios (e.g. when toggled from minimized to maximized [and vice versa] and when the page is first loaded), we can create a method just for this action. We'll call this method setViewState.
  2. Each tab will need to format the RSS or ATOM feed for display to the end user, so we'll give that it's own method and call it xmlReturnFilter.

Now that we've defined the structure of our object, let's see how that looks in code. Our object will be called RssAtomJquery:


var RssAtomJquery = {
  /**
   * Define constants for cookies
   */
  SETTING_MINIMIZED:1,
  SETTING_MAXIMIZED:2,

  referenced_feeds:[],
  current_settings:RssAtomJquery.SETTING_MINIMIZED,

  init:function () {},
  setViewState:function (view_state) {},
  showMinimized:function () {},
  showMaximized:function () {},
  tabLoaded:function () {},
  jumpToSelectedFeed:function () {},
  xmlReturnFilter:function () {}
}
    

Before we start filling out our object, let's add a couple of lines at the end of that script to initialize the view for users when they load the page and when they resize the window:


$(document).ready(function () { RssAtomJquery.init(); });
$(window).resize(function(){ RssAtomJquery.setViewState(RssAtomJquery.current_settings); });
    

The first line above calls our object's init method and the second line assumes that setting the view state to its current state will cause the screen to be redrawn (so we'll need to keep that in mind when creating the method).

Our "final" step is to flesh out our methods.

METHOD: init

Our initialization method has 4 tasks to perform:

  1. Fetch all <link> tags for the document and store them for later use.
    
    // Find all <link> tags whose "type" attribute begins with
    // "application/" and ends with "+xml"
    var auto_referenced_feeds = $('link[type^=application/][type$=+xml]');
    if (auto_referenced_feeds && auto_referenced_feeds.length > 0) {
      for (var i=0;i<auto_referenced_feeds.length;i++) {
        //Add feed title and URL to our private collection for quick reference
        this.referenced_feeds.push({'url':
          auto_referenced_feeds[i].href,'title':auto_referenced_feeds[i].title
        });
      }
    }
        
  2. Ensure that current_settings is initialized.
    
    //Initialize current settings
    if (this.current_settings == null) {
      this.current_settings = this.SETTING_MINIMIZED;
    }
        
  3. Add our main container to the stage for displaying each view state.
    
    //Ensure editable window is on page.
    if (!$('#raj_window').attr("id")) {
      //Window not present, so insert with id raj_window
      $('<div id="raj_window"></div>').appendTo("body");
      //Set raj_window's class to raj_hidden
      $('#raj_window').attr("class", 'raj_hidden');
    }
        
  4. Call setViewState to paint the correct initial view.
    
    //Perform initial actions based on settings
    this.setViewState(this.current_settings);
        

METHOD: setViewState

When setting the current view, there are only 2 tasks to perform:

  1. Save the selected view in a cookie for future reference.
    
    //Typecast to ensure compatibility with constants type
    view_state = parseInt(view_state);
    
    //Update stored value
    $.cookie('RAJ_v', view_state, {expires:100});
    this.current_settings = view_state;
        
  2. Show the correct view.
    
    switch (view_state) {
      case this.SETTING_MINIMIZED:
        this.showMinimized();
        break;
      case this.SETTING_MAXIMIZED:
        this.showMaximized();
        break;
    }
        

METHOD: showMinimized

For the minimal view, we'll just be showing an RSS icon in the upper-right corner. There are 3 tasks necessary here:

  1. Set the size and position of the icon using our raj_window container.
    
    //Create layout for window
    var window_size = [50,50];
    var window_margin = 10;
    $('#raj_window').css("top", window_margin + "px");
    $('#raj_window').css("left", ($(window).width() -
        window_size[0] - window_margin) + "px"
    );
    $('#raj_window').css("width",  window_size[0] + "px");
    $('#raj_window').css("height", window_size[1] + "px");
        
  2. Add our icon and set it to toggle the view state using setViewState.
    
    //Add content for minimized version
    $('#raj_window').html('<div onClick="RssAtomJquery.setViewState('
      + RssAtomJquery.SETTING_MAXIMIZED
      + ');"
      style="cursor:pointer;"><img src="feeds.png" alt="Feeds" /></div>');
        
  3. Update the raj_window class for easy CSS styling.
    
    //Change class
    $('#raj_window').attr("class", 'raj_minimized');
        

METHOD: showMaximized

Now we get into the "heart" of our script, the maximized view. Again, however, by breaking down the tasks of this method, we'll be able to easily harness jQuery.
  1. As with the showMinimized method, we begin showMaximized by sizing and positioning the raj_window container to be centered in the window and 90% of the height and width.
    
    //Create layout for window
    var window_size = [parseInt($(window).width() * .9),parseInt($(window).height() * .9)];
    $('#raj_window').css("top", parseInt(($(window).height() - window_size[1])/2) + "px");
    $('#raj_window').css("left", parseInt(($(window).width() - window_size[0])/2) + "px");
    $('#raj_window').css("width",  window_size[0] + "px");
    $('#raj_window').css("height", window_size[1] + "px");
          
  2. Next, we create the tab navigation. To do this, we'll iterate over our stored variable referenced_feeds to use the title attribute as the tab's label (or the URL if the title attribute is not set). With jQuery, it's a breeze to make our tabs automatically load the desired RSS or ATOM feed by simply creating the LI with a link as the content.
    
    //Add content for maximized version
    //Create tabs and content boxes for each feed
    var tab_nav = tab_content = display_text = '';
    for (var i=0;i<this.referenced_feeds.length;i++) {
      if (this.referenced_feeds[i].title.length == 0) {
        display_text = this.referenced_feeds[i].url.substr(
          this.referenced_feeds[i].url.lastIndexOf("/")
          - this.referenced_feeds[i].url.length + 1);
      } else {
        display_text = this.referenced_feeds[i].title;
      }
      tab_nav += '<li><a href="' + this.referenced_feeds[i].url + '">'
        + display_text + '</a></li>';
    }
          
  3. Next, we add in the navigation LI's created above and place them in a container called raj_feed_tabs.
    
    $('#raj_window').html('<div id="raj_feed_tabs"><ul id="raj_tab_nav">' + tab_nav + '</ul></div>');
          
  4. Next, we set our feed container to take up the entire raj_window area.
    
    
    $('#raj_feed_tabs').css("height", $('#raj_window').height() + "px");
    $('#raj_feed_tabs').css("width", $('#raj_window').width() + "px");
          
  5. Moving along, we ask jQuery to create the tab interface and bind the tabs to xmlReturnFilter so that we have a chance to format our feed before being displayed in the tab.
    
    $('#raj_feed_tabs').tabs({
                              load: this.tabLoaded,
                              ajaxOptions: {dataType: 'xml',
                                            dataFilter:this.xmlReturnFilter}
                            });
          
  6. Now we set the raj_window class to raj_maximized.
    
    //Change class
    $('#raj_window').attr("class", 'raj_maximized');
          
  7. For a slight touch of class and user-friendliness, we're going to piggy-back off of the jQuery dialog widget to add a close button to our tab interface. This code was tweaked from the jQuery code used in the dialog widget.
    
    //Add Close button
    $('<span id="tab_close" class="ui-dialog"></span>').appendTo('#raj_feed_tabs');
    uiDialogTitlebarClose = $('<a href="#"/>')
    		.addClass(
    			'ui-dialog-titlebar-close ' +
    			'ui-corner-all'
    		)
    		.attr('role', 'button')
    		.hover(
    			function() {
    				uiDialogTitlebarClose.addClass('ui-state-hover');
    			},
    			function() {
    				uiDialogTitlebarClose.removeClass('ui-state-hover');
    			}
    		)
    		.focus(function() {
    			uiDialogTitlebarClose.addClass('ui-state-focus');
    		})
    		.blur(function() {
    			uiDialogTitlebarClose.removeClass('ui-state-focus');
    		})
    		.mousedown(function(ev) {
    			ev.stopPropagation();
    		})
    		.click(function(event) {
    			RssAtomJquery.setViewState(RssAtomJquery.SETTING_MINIMIZED);
    			return false;
    		})
        .prependTo('#tab_close');
    
    uiDialogTitlebarCloseText = (this.uiDialogTitlebarCloseText = $('<span/>'))
    		.addClass(
    			'ui-icon ' +
    			'ui-icon-closethick'
    		)
    		.text('close')
    		.appendTo(uiDialogTitlebarClose);
    
    $('#tab_close').css("left", (
      parseInt($('#raj_tab_nav').width())
      - parseInt($('#tab_close').width())
    ) + "px");
          
And, with quite a few more steps than our other functions, we now have the showMaximized method implemented!

METHOD: tabLoaded

In our showMaximized method, we bound the tab widget to this function. Here, we'll simply layout the tab with the list of items on the left as a selectable widget and the message bodies on the right.

//Get the selected tab
var selected = $('#raj_feed_tabs').tabs('option', 'selected');

//Set the height of the items list and message body sub-windows.
$('#raj_items_list_' + selected).css("height", parseInt(
  ($('#raj_window').height() - $('#raj_tab_nav').height() - 20) * .95)
  + "px");
$('#raj_message_' + selected).css("height", parseInt(
  ($('#raj_window').height() - $('#raj_tab_nav').height() - 20) * .95)
  + "px");

//Set the width of the items list and message body sub-windows.
$('#raj_items_list_' + selected).css("width", parseInt(
  ($('#raj_window').width() - 50) * .25)
  + "px");
$('#raj_message_' + selected).css("width", parseInt(
  ($('#raj_window').width() - 50) * .75)
  + "px");

//Make the items list selectable and bind to jumpToSelectedFeed
$('#raj_items_list_' + selected).selectable( { selected: RssAtomJquery.jumpToSelectedFeed });
    

METHOD: jumpToSelectedFeed

This method simply scrolls the correct body into view when an item in the items list is selected.

jumpToSelectedFeed: function (event, ui) {
  var selected = $('#raj_feed_tabs').tabs('option', 'selected');

  document.getElementById(ui.selected.id.replace(/hash/,'xml_header')).scrollIntoView(true);
}

METHOD: xmlReturnFilter

After everything above, we are now ready to use jQuery to parse our ATOM and RSS feeds when returned by the tab widget. This is a rather straight-forward process as well:
  1. We determine the feed type;
  2. Extract the title, link, and description; and
  3. Add the item to the item list and a description to the bodies box
I've kept the code as a single block for easier reading and added in comments to show where each of these steps are implemented:

xmlReturnFilter: function(r, s) {
  if (r.getElementsByTagName('item')) {
    //Potentially a feed we can use
    
    //Initialize variables, get the selected tab, and attempt to fetch all <item> entries
    var selected = $('#raj_feed_tabs').tabs('option', 'selected');
    var filtered_response = raj_items_list = message = title = link = description = '';
    var jXml = r.getElementsByTagName('item');

    if (jXml.length == 0) {
      //Cannot be an RSS feed, so attempt to fetch all <entry> entries
      jXml = r.getElementsByTagName('entry');
      if (jXml.length > 0) {
        // ATOM feed, so traverse over all results and fetch title, link, and description
        // (using the ATOM summary tag for the description)
        for (var i=0;i<jXml.length;i++) {
          title = ((jXml[i].getElementsByTagName('title')[0].text) ?
            jXml[i].getElementsByTagName('title')[0].text
            :
            jXml[i].getElementsByTagName('title')[0].textContent);
          link = jXml[i].getElementsByTagName('link')[0].getAttribute("href");

          description = ((jXml[i].getElementsByTagName('summary')[0].text) ?
            jXml[i].getElementsByTagName('summary')[0].text
            :
            jXml[i].getElementsByTagName('summary')[0].textContent);

          //Add item to items list and create "filtered_response" to hold our formatted body element
          raj_items_list += '<li id="raj_hash_' + selected + '_' + i + '">' + title + '</li>';
          filtered_response += '<div id="raj_xml_header_' + selected + '_'
            + i + '" class="raj_xml_header raj_atom_header"><a href="'
            + link + '">' + title + '</a></div>';
          filtered_response += '<div class="raj_xml_body raj_atom_body">'
            + description + '</div>';
        }
      } else {
        //Not a feed we are working with, so return untouched data
        return r;
      }
    } else {
      //RSS feed parsing so traverse over all results and fetch title, link, and description
      for (var i=0;i<jXml.length;i++) {
        title = ((jXml[i].getElementsByTagName('title')[0].text) ?
          jXml[i].getElementsByTagName('title')[0].text
          :
          jXml[i].getElementsByTagName('title')[0].textContent);
        link = ((jXml[i].getElementsByTagName('link')[0].text) ?
          jXml[i].getElementsByTagName('link')[0].text
          :
          jXml[i].getElementsByTagName('link')[0].textContent);
        description = ((jXml[i].getElementsByTagName('description')[0].text) ?
          jXml[i].getElementsByTagName('description')[0].text
          :
          jXml[i].getElementsByTagName('description')[0].textContent);

        //Add item to items list and create "filtered_response" to hold our formatted body element
        raj_items_list += '<li id="raj_hash_' + selected + '_' + i + '">' + title + '</li>';
        filtered_response += '<div  id="raj_xml_header_'
          + selected + '_' + i
          + '" class="raj_xml_header raj_rss_header"><a href="' + link
          + '">' + title + '</a></div>';
        filtered_response += '<div class="raj_xml_body raj_rss_body">'
          + description + '</div>';
      }
    }
    // Create items list and bodies and return re-formatted response for displaying in our tab
    // (note: formatting will take place in the tabLoaded function)
    return '<div id="raj_feed_box"><ol id="raj_items_list_'
      + selected + '" class="raj_items_list">' + raj_items_list
      + '</ol><div id="raj_message_' + selected + '" class="raj_message">'
      + filtered_response
      + '</div></div>';
  } else {
    //Not a feed we can use, so just return the original server response
    return r;
  }
}
  

We have now completed our standalone class! By adding a reference to the class file to any javascript page, we are able to have our ATOM and RSS feeds automatically displayed to our visitors in a user-friendly fashion without them leaving to go to their aggregator.

Now a very logical question one might ask is, "And why would I want to write my own feed reader for my webpage when people can visit their aggregators?" To which, I just might reply, "Have you thought about making the feed based on how long the user was sitting on your page, how often they return, how long inbetween visits, ...? If you have, then you could offer targeted entries (e.g. discounts, tips, welcome back, ...) to catch your users' attention, close a sale, or keep them coming back." — We really have given a bit of extra personalization to a site by creating our own reader and that means more repeat visits and greater sales.

Full example file set

Author

by: Jeremy Miller
Jeremy graduated magnua cum laude from Humboldt State University in 2004 with a BA in Mathematics and a minor in Computer Information Systems. He currently runs his own company, TeraTask and serves as a super moderator on Webmaster-Talk. His services include systems analysis and application development.
Tech Tags:


Sponsors:

About willCode4Beer