jQuery XML Parser with Sorting and Filtering

Note: This script was originally written in January 2009 but has been re-written in jQuery plugin format as of Aug 2012. The project has also been moved to GitHub.

In early 2009, I experimented with XML based iPhone / Safari Mobile web app which used jQuery to parse and traverse multiple levels of nodes in an XML file and display the data. While that project never panned out, the core code has become quite useful in a number of professional and personal projects. This tutorial provides working examples and demonstrates the concept of parsing and traversing an XML file with jQuery using AJAX.

View DemoDownload Source

The XML

First, lets take a look at the XML structure:

<?xml version="1.0" encoding="UTF-8"?>
<data>
	<entry date="1/08/10" cost="27" category="Mobile Phone Accessories">
		<name>
			<![CDATA[ iPhone Case]]>
		</name>
		<description>
			<![CDATA[ Black Matte Finish, fits all 3G and 3Gs models. ]]>
		</description>
	</entry>
 
</data>

In it’s most basic form, the XML file contains the main <data> node with subsequent <entry> nodes. Within each entry we are defining the meta data such as <date>, <cost> and <category>. Also within the entry are a <name> and <description> field which are wrapped in CDATA blocks that will allow us to use special characters and punctuation without compromising the validity of the XML file.

AJAX

Using jQuery’s wonderfully simple AJAX method to make an XMLHttpRequest. The basic code for making the AJAX request for the example XML file shown below:

$.ajax({
type: 'GET',
url: 'xml/data.xml',
dataType: 'xml',
success: function(xml_list) {</code>
 
var xmlArr = [];
 
$(xml_list).find('entry').each(function() {
 
	var xml_date      	= $(this).attr('date');
	var xml_cost 	  	= $(this).attr('cost');
	var xml_category  	= $(this).attr('category');
 
	var xml_name  	  	= $(this).find('name').text();
	var xml_description = $(this).find('description').text();
					  // Add matched items to an array
	xmlArr += '&lt;tr filterCriteria="';
	xmlArr += xml_cost;
	xmlArr += '"&gt;&lt;td&gt;';
	xmlArr += xml_date;
	xmlArr += '&lt;/td&gt;&lt;td class="xml_name"&gt;';
	xmlArr += xml_name;
	xmlArr += '&lt;/td&gt;&lt;td&gt;';
	xmlArr += xml_description;
	xmlArr += '&lt;/td&gt;&lt;td&gt;';
	xmlArr += xml_category;
	xmlArr += '&lt;/td&gt;&lt;td class="xml_cost"&gt;$';
	xmlArr += xml_cost;
	xmlArr += '&lt;/td&gt;&lt;/tr&gt;';
 
}); // end each loop
 
 //Append array to table (this way is much faster than doing this individually for each item)
 
	$(xmlArr).appendTo(wrapper +' table tbody');

The first two things to note in the code are the dataType and success parameters. We use dataType to specify that we are requesting an XML fie and using a function in success to manipulate and display the XML data. Next, we use jQuery’s super handy find() method to search the XML document for entry nodes. We then chain the each() function to iterate through the data that is returned from the find(). Next, variables are created for each of the items we will be displaying. Using variables as opposed to the selector name caches the data so that it is not called in each iteration. Note that there are two methods of assigning the variables in this example; the first simply assigns available attributes to the variables while the second has to traverse one step deeper to return the values of the name and description nodes.

Making it work

At this point we’ve covered the basic concepts but it is time to actually do something practical with the data. Displaying the results in HTML is easy but while we’re writing the markup, lets add some placeholders for filtering the data.

<div id="xml_wrapper">
<ul id="xml_nav">
	<li><a class="filter_10" href="javascript:void(0)"><span>&lt; $10</span></a></li>
	<li><a class="filter_10_20" href="javascript:void(0)"><span>$10 – $20</span></a></li>
	<li><a class="filter_20" href="javascript:void(0)"><span>$20 +</span></a></li>
	<li class="xml_nav_hit"><a class="filter_0 hit" href="javascript:void(0)"><span>Any price</span></a></li>
</ul>
<table border="0">
<thead>
<tr>
<th class="header headerSortUp">Date</th>
<th class="header">Name</th>
<th class="header">Description</th>
<th class="header">Category</th>
<th class="header">$</th>
</tr>
</thead>
<tbody></tbody></table>
</div>

Ignore the <UL> for now and notice that we are adding elements to the the <thead> but not the <tbody>. Jump back to the JavaScript file and add this line below the var declaration for description and add:

$('&lt;tr filterCriteria= "'+ xml_cost +'"&gt;&lt;/tr&gt;')
.html('<td>'+ xml_date  +'</td><td class="xml_name">'+ xml_name  +'</td><td>'+ xml_description  +'</td><td>'+ xml_category +'</td><td class="xml_cost">$'+ xml_cost  +'</td>')
.appendTo(wrapper +' table tbody');

This line is in our loop so it will iterate a populated <tr> for each entry in our XML and append it to the <tbody></tbody> of the table.

Making it useful

The parser is now built and displays the data from our XML file but it lacks practicality. What if you want to sort by Computer Accessories? What if you want to show all items less than $20? These start to become serious issues as the length of the XML document increases.

We will cheat a little bit and tie in Christian Bach’s incredibly useful tablesorter jQuery plugin to do the sorting in this example. I won’t go into detail on the options and uses of this plugin because they are already covered thoroughly on the tablesorter plugin site.

Filtering is done by simply using jQuery’s hide() and show() on specifically tagged table rows. We’ve already created the HTML in previous steps so all that is left is to assign a onClick event and add a switch to create the filter. To add sorting to the table we’ve created add the following after the each() function inside the AJAX call:

 window.setTimeout('$("'+ wrapper +' table").tablesorter({sortList:[[0,0],[0,0]], widgets: ['zebra']});', 120);

The selector is wrapped in an interval because both the tablesorter script call and our AJAX call happen concurrently on page load which causes the tablesorter to fire before the table is fully populated with or XML. The 120ms on the interval is somewhat arbitrary and it can be changed as needed. Additionally, if you prefer not to use the interval, you might be able to do so depending on your particular use and XML file size. Also, note the use of the “zebra widget” option; this will assign a “stripe” class to odd numbered rows which allows for alternate row background colors.

var nav_link = $('#xml_nav li a');
nav_link.click( function() {
var tr = wrapper +' table tbody tr';
$(tr).show(); //Show all rows
switch ($(this).attr("class")) {
case  "filter_10 hit" :
$(tr).filter(function (index) {
return parseFloat($(this).attr('filterCriteria')) &gt; 10;
}).hide();
break;
 
case  "filter_10_20 hit" :
$(tr).filter(function (index) {
return parseFloat($(this).attr('filterCriteria')) &lt; 10 || parseFloat($(this).attr("filterCriteria")) &gt; 20  ;
}).hide();
break;
 
case  "filter_20 hit" :
$(tr).filter(function (index) {
return parseFloat($(this).attr('filterCriteria')) &lt; 20;
}).hide();
 
break;
 
case  "filter_0 hit" :
$(tr).show();
break;
 
}
/* Remove all instances of the stripe (alternating row) class
   Then re-apply stripe class as to visible alternating rows
*/
$(tr).removeClass('stripe');
$(tr + ':visible:odd').addClass('stripe');
});

In a previous step we generated the HTML that is inserted into our table and you might have noticed an attribute was added to the cost <td> called filterCriteria. The code above uses the value of this attribute to determine which items to show / hide. Obviously, this will only work if the value of cost is an integer. Feel free to develop a more elegant solution.

Finishing up

The last step that I will cover is adding a simple preloader to display as AJAX grabs the XML data. At the top of the function place the following line:

     $('<div id="preload_xml"></div>').html('<img src="images/ajax-loader.gif" alt="loading data" /><h3>Loading Data...</h3>').prependTo($('body'));

Inside of the xml_parser() function in the AJAX call’s success parameter place these two lines:

$('#preload_xml').remove();
$(wrapper).show();

Thes four lines of code will first hide our wrapper div and then dynamically construct a div with a loading message and then remove the code from the page once everything is loaded and finally show the wrapper again.

We now have a parser that sorts and filters XML data. There are many other features that could be added such as a more complex content filter and code cleanup to make things even more re-usable. If you find this code useful and decide to use it in your projects, I encourage you to modify the code as you find necessary and please feel free to share your examples.

Other Considerations

There is a great jQuery XML parser plugin called jParse which can be used to grab display the contents of an XML file with little effort. Other than the reliance on AJAX to call the XML file, the approach used by jParse differs considerably from the approach that I took in this article. If you are looking strictly for XML display, I recommend looking into jParse.

Note: This script was originally written in January 2009 but has been re-written in jQuery plugin format as of Aug 2012. While the concepts are the same, most of the parser code has be re-written since this article was published. Please reference the GitHub repo for the latest code.

View DemoDownload Source

26 comments on “jQuery XML Parser with Sorting and Filtering

  1. Hi, thanks for a great job?

    I Have too questions. First I want to sort my data by category’s instead of prices, could you please help me with the code?

    Second, I wonder if you have a quick fix to link “Name” to a link in the XML-feed?

  2. dear Ben,

    another question,

    the parser works offline for firefox and safari: how can we use it offline for IE?

    thanks!

    m.c.

    • hi all,

      When loading XML files locally, e.g. a CD-ROM etc., the data received by Internet Explorer is plain-text, not text/xml. For this reason this parser doesn’t work.

      In this case you have to change the mime type as following:

      $.ajax({
      url: “data.xml”,
      dataType: ($.browser.msie) ? “text” : “xml”,
      success: function(data){
      var xml;
      if (typeof data == “string”) {
      xml = new ActiveXObject(“Microsoft.XMLDOM”);
      xml.async = false;
      xml.loadXML(data);
      } else {
      xml = data;
      }
      // Returned data available in object “xml”
      }
      });

      source: jquery docs
      http://docs.jquery.com/Specifying_the_Data_Type_for_AJAX_Requests

  3. Hi Ben, great job. thanks.
    i’m using the parser for an off-line catalog.

    i wounder if you have any tip about a keyword searching through the xml data.

    i’m also trying to present 15 or 20 rows for each page. do you know any example i can use?

    thank you for any help

    m.

  4. Hi Ben,

    Fabien again with a question: do you think is it possible to make a sorting by alphabetic when the page is loading?

    • Hello Fabien,

      You can set the initial sort column within the Tablesorter plugin options. See: http://tablesorter.com/docs/example-option-sort-list.html for details.

      I know it is possible that your data could load before the Tablesorter plugin is instantiated which would cause the data to re-sort on load but probably your only options to fix that are to hide the sort table until you know the Tablesorter has run or manually sort the XML.

  5. Hi Ben, I’m new in Jquery. I want add the select (Combo box) and input box , so people can filter as they like. How to implement it in jquery. Thanx

  6. Hi Ben,
    I am happily using your xml parser (haven’t found anything more elegant in its category).
    What I would like is to be able to include multiple classes in a criteria (say, category=”english french canada usa europe”) and then ask the parser to filter (show) only those entries which would include say, canada and french. How can I modify your code to enable this? Right now I am only able to filter data that is not a specific class and can only use one item (class) per category, like so:

    xml:
    parser:
    case “canada hit” :
    $(tr).filter(function (index) {
    return $(this).attr(‘filterCriteria’) != “canada” ;
    }).hide();
    break;
    which hides all rows whose category is not equal to “canada”. What I want is:

    xml:
    parser:

    case “canada hit” :
    $(tr).filter(function (index) {
    return $(this).attr(‘filterCriteria’) [DOES NOT INCLUDE] “canada” ;
    }).hide();
    break;
    Thanks.

    • This could be done a number of ways but I would suggest looking into jQuery’s contains() selector.

      Off the top of my head, to do [DOES NOT INCLUDE] you could do something like:


      $(tr).filter(function (index) {
      return $(this).attr(’filterCriteria’).not(‘contains:(“Canada”)’);
      }).hide();
      break;

      Alternatively, you could probably write a regular expression to do this (which would probably be more efficient).

  7. Is it possible to use access db instead of xml? Im really new here so don’t know how to do it, if it’s possible.

    • This is very possible but it would require quite a few modifications. I would either format the DB data as JSON and then handle that with jQuery or use AJAX to hit a page that pulls your data.

  8. Hi
    Thanks for this its working great for me but how would I display a image from the xml file in the html page its just placed the URL
    any help would br great

    • Robotech, I took a look at your link and you are pretty close.

      Change these lines:

      xmlArr += '</td> <td class="xml_image1"> imageObject.src=""';
      xmlArr += xml_image1;

      To something like this:
      xmlArr += '</td><td class="xml_image1"> <img src="'+ xml_image1 +'" alt="" />';

      Basically you just need to wrap the xml_img1 variable in an image tag’s src attribute and when the script adds the formatted XML to your page the it will render the images as HTML.

    • Thanks Ben that worked to display the image but the format is off all text strats at the bottob of the image and I cannot seem to ajust it any ideas

      Robotech

    • It looks like you just need to vertically align the TD data. In the CSS, #xml_wrapper table td you should add either vertical-align: top; or vertical-align: middle;. Additionally, you should add a </tr> after you insert the <img /> in the XML.JS file

    • Thanks Ben that works great

      now it works on a small test xml file datamy.xml all looks great but when i change it to the large xml file data2.xml which has over 500 records it will not load is there some setting that need to be changed for this many records?
      Also can you read the xml file from a different url or does it need to be local?

    • My guess is that there is a syntax error in your “data2.xml” file. I’ve used this script with close to 1000 records before without any problems other than it taking a while to load. The other thing to check is the path to your files, it is a bit tricky because although the XML.js file is in the JS folder, the XML file is actually linked from the XML folder in the root by default, e.g. xml/data2.xml rather than ../xml/data2.xml.

  9. How could we do to insert more TR, by exemple, I’d like to have the XML results on Two lines ?
    Thanks.

    • Hi Fabien,

      You could do this by adding more HTML to the append(). The append section currently writes a TR and then adds HTML to it and appends it to the table. What you would want to do is combine this action so it would look more like:

      $('YOUR CONTENT..ANOTHER ROW'S CONTENT...').appendTo('wrapper +' table tbody'');

      This will technically work BUT this script contains a sorting filter so you will need to make sure that the additional rows match the filter criteria or they will not appear in the correct order. Alternatively, you can just remove the filter code and just display your data.

      One other thing to note: I just updated the demo / sample code make the append more efficient. The script now first stores the table row data in an array and then appends them all one time rather than with each iteration of the loop.

      Hope that helps!

      -Ben

    • Ho yes, many thanks, I found that we could remove the filter, that’s more easy to use lot of data. this would it be helpufull to have var of the data to filter… but you script is very usefull.

      When I will have finished this first page, I will try to make TR in the content, my first try didn’t work, I think we must have the same number of coloumn that the number of filter.

      What we can do to have more rows: use DIV.

      Ben, I will send you url when it will be online.
      Thanks !!!

      Fabien.

    • Another question Ben, How can we do for “row match the filter”?

      By example:
      I try to have results in two rows, and colspan on the second one.
      But the filters does not works.

      Here are the code:

      // append data / HTML to source table
      $(”).html(”+ xml_date +”+ xml_horaires +”+xml_societe +”+ xml_titre +”+ xml_nom +”+ xml_prenom +”+ xml_fonction +”+ xml_resume +”).appendTo(wrapper +’ table tbody’);
      }); // end each loop

      Here the html code:

      Date
      Horaire
      Société

      Do you think there is another solution?

      Thanks again.

    • Fabien:

      If you change the filter criteria from the integer value I had it set at you will need to re-work the code but if you are still using that same field you would just need to add an additional filter for the next row.

      In the switch statement you would want to add a line similar to this ON TOP of the existing code:

      $(tr).filter(function (index) {
      return parseFloat($(this).attr(‘filterCriteria’)) > 10;
      }).next(‘tr’).hide();

      Notice the next(‘tr’) which will target the 2nd (next) row.

      Also, you might want to make a slight tweak on the code you pasted:

      // append data / HTML to source table
      $(”+ xml_date +”+ xml_horaires +”+xml_societe +”+ xml_titre +”+ xml_nom +”+ xml_prenom +”+ xml_fonction +”+ xml_resume +”).appendTo(wrapper +’ table tbody’);
      });
      // end each loop

      This will avoid inserting blank code on each iteration.

      I would love to see the finished product, make sure to post a link!