Introduction
The procedure described here will enable you to create a simple jQuery/XML based parser and search mechanism. This procedure will retrieve the XML through an AJAX request and then parse the data within jQuery to prepare it for the search mechanism. This solution will return results based on case insensitive full or partial keyword matches. The returned result set from the keyword search will be formatted as a direct hyperlink(s) to the corresponding sites. The methods of the jQuery search are very similar to a project by Mike Endale with the additions of a DOM parser, RegExp, and result set grouping.
Background
A client required a simple search tool to locate internal websites based on keyword searches. The keyword searches must be case insensitive and allow for partial match results to be returned. Due to the architecture of the client’s content management system, (SharePoint) only client-side scripts can be executed. Another obstacle for his solution is that the source data will come from multiple sources. The data is stored within multiple Excel spread sheets, CSV files and an MS Access database. This presented the need to develop an Access solution with a series of queries and a macro to function as a pseudo ETL that will merge, scrub and finally format the data as XML output. For the purpose of this solution we will detail the design of the JavaScript XML parser and not the design of the pseudo Access ETL macro tool.
Using the code
The solution approach will utilize a simple JavaScript/XML based search to send the data results to a HTML/JavaScript frontend. The frontend will reference the scripts: jQuery, XML and CSS files. The XML format will be utilized because of its readability and the fact that it’s one of the industry standard formats for data interchange. The XML data will be parsed through client-side jQuery using AJAX and presented through Internet Explorer 11.
The solution will use RegExp object to handle the keyword match, validation, and special character handling. The RegExp object string will be checked for dangerous syntax thereby improving the stability and overall usability of the solution.
We will use a JavaScript grouping functionality to return matching results as a collapsed record set by default. The collapsed record set line item will be the URL link to the associated Project Workspace website. Under the expanded group record set results will reside associated child records when expanded by On Click event.
Information Architecture
The parser function takes a complex hierarchical XML tree with nodes and attributes, and turns it into equivalent JavaScript objects and properties. The client-side JavaScript/XML based search goes through the following steps:
- The pseudo ETL tool prepares the data into an XML file (this is not covered in this project)
- The XML file is loaded to a specified location (this is not covered in this project)
- Upon a click event the JavaScript parser will load the XML data using AJAX method
- Check for the presence of keyword(s) for the search
- If no keyword(s) are present throw error message "Please enter a search keyword"
- If a node contains a string for the URL attribute then those nodes are brought into an array.
- RegExp object keyword match with special character handling by replacement
- RegExp object keyword match transformed to case insensitive
- Loop the array to match up based on the validated RegExp object
- If no result(s) are throw error message "No results were found!"
- Build result set with zebra striped even and odd rows for the top level group
- Build group matching PPID rows with associated Work Order(s) as sub groups
- Populate the results then pass them to the final set to be presented
- Show the result set with the titles of the columns and all groupings
- Groupings are collapsed by default equivalent
User Interface
The user interface will be a simple HTML/JavaScript client-side based search to return keyword matched results as collapsed grouped record set by default. The collapsed record set in line item(s) will be the direct URL link to the associated project website. Under the expanded group record set results will reside associated child records when expanded by On Click event.
Inline Page References
First thing we will need to do is reference our scripts: jQuery, XML and CSS files.
< link rel="stylesheet" href="path/default.css" />
< script type="text/javascript" src="path/jquery-1.4.2.min.js"></script>
< script id="data" type="text/javascript" src="path/search.js" xmlData="data.xml"></script>
< input id="term" type="text"/> < input name="Search" id="searchButton" type="button" value="Search"/>
< div id="result">< /div>
You’ll notice we’ve added the xmlData
attribute to the search.js reference. This is the best way to pass the XML file location from the HTML file. This helps a great deal if you have multiple xml files you want to use as data source.
XML Data Sources
The XML data source can be structured in any way or can be any size; but it is recommended to keep source XML files under 1 MB to maintain an appropriate parser response time. Here is an example of the XML source used for this project:
< ?xml version="1.0" encoding="UTF-8"?>
< dataroot generated="2015-11-20T10:30" xmlns:od="urn:schemas-microsoft-com:officedata">
< etl>
< PPID Lead="Slow,Roy" Description="NORTH OF FAIR" PID="P002">
< WO Description="SHELTON - BANK (SAFETY)" PM="Slow,Roy" Status="CLOSED" WID="305577" WOXREF="SHEL" Program="REINFORCEMENT">
< /WO>
< Archive>Archived</Archive>
< record search="P002NORTH OF FAIRSHELSHELTON 305577SHELTON - BANK (SAFETY)Slow,Roy"/>
< url address="P002"/>
< /PPID>
< /etl>
< /dataroot>
Error Handling
For the purpose of this project we have utilized error handling in two critical areas. If no keyword(s) are present then the error message "Please enter a search keyword" will be presented. If no result(s) are generated then throw the error message "No results were found!" will be presented.
if (keyword == '') {
errMsg += 'Please enter a search keyword';
} else {
searchThis();
}
if (i == 0) {
pub += '< div class="error">';
pub += 'No results were found!';
pub += '< /div>';
Using the jQuery AJAX Request
We will call the XML through an Asynchronous JavaScript function through the predefined jQuery library that was enabled on page level above. AJAX stands for "Asynchronous JavaScript and XML", and was coined by Jesse James Garrett, founder of Adaptive Path. AJAX relies on XMLHttpRequest, CSS, DOM, and other technologies. The main characteristic of AJAX is its "asynchronous" nature, which means it can send and receive data from the server without having to refresh the page. With asynchronous mode, the client and the server will work independently and also communicate independently, allowing the user to continue interaction with the web page, independent of what is happening on the server.
function searchThis() {
$.ajax({
type: "GET",
url: XMLSource,
dataType: "xml",
success: function (xml) {
loadPublication(xml)
}
});
}
Using the DOM Parsing and RegExp
Since jQuery itself is not capable of parsing XML strings; we will leverage the browsers DOM parsing method which most browsers support in one form or another. This method is supported by Firefox, Chrome, Safari and the latest Internet Explorer browsers that have a DOMParser object. Older Internet Explorer browsers (like IE 8) use their proprietary ActiveX object. A cross-browser solution can be created that checks for the absence of the DOMParser, but that is out of scope for this project but may be added at a later date for additional cross-browser support.
Knowing the RegExp (Regular Expression) capabilities and syntax of JavaScript I wanted to have a simplified function within RegExp in my source to deal with any special characters. We also wanted the Regex to be case insensitive through defining the RegExp to ignore case for a more user friendly keyword searches.
function loadPublication(xmlData) {
i = 0;
var row;
var searchExp = "";
var ppid = "P";
$(xmlData).find('PPID').each(function () {
if($(this).find('url').attr('address').length) {
var record = $(this).find('record').attr('search');
var archive = $(this).find('Archive');
RegExp.escape = function(keyword) {
return keyword.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
var exp = new RegExp(keyword, "i");
searchExp = record.match(exp);
if (searchExp != null) {
if ((i % 2) == 0) {
row = 'even';
} else {
row = 'odd';
}
if($(this).attr('PID') != ppid) {
ppid = $(this).attr('PID');
i++;
Grouping the Returned Results
The user interface will be a simple HTML/JavaScript client-side based search to return keyword matched results as collapsed grouped record set by default. The collapsed record set in line item(s) will be the direct URL link to the associated Project website. Under the expanded group record set results will reside associated child records when expanded by On Click event.
function expgroupby(e) {
docElts=document.all;
numElts=docElts.length;
images = e.getElementsByTagName("IMG");
img=images[0];
srcPath=img.src;
index=srcPath.lastIndexOf("/");
imgName=srcPath.slice(index+1);
var b="auto";
if(imgName=="plus.gif"){
b="";
img.src="/_layouts/images/minus.gif"
}else{
b="none";
img.src="/_layouts/images/plus.gif"
}
oldName=img.name;
img.name=img.alt;
spanNode=img;
while(spanNode!=null){
spanNode=spanNode.parentNode;
if(spanNode!=null&&spanNode.id!=null&&spanNode.id=="wrapper")break
}
while(spanNode.nextSibling!=null&&spanNode.nextSibling.id!="wrapper"){
spanNode=spanNode.nextSibling;
spanNode.style.display=b;
}
}
Full Source Code
For the purpose of this project here is the example of the full source code.
$(document).ready(function () {
var XMLSource = $('#data').attr('xmlData');
var keyword = '';
var pub = '';
var i = 0;
$("#searchButton").click(function () {
keyword = $("input#term").val();
var errMsg = '';
pub = '';
if (keyword == '') {
errMsg += 'Please enter a search keyword';
} else {
searchThis();
}
if (errMsg != '') {
pub += '< div class="error">';
pub += errMsg;
pub += '< /div>';
}
$('#result').html(pub);
});
$("input#term").keypress(function (e) {
var key = e.which;
if (key == 13){
$("#searchButton").click();
return false;
}
});
function searchThis() {
$.ajax({
type: "GET",
url: XMLSource,
dataType: "xml",
success: function (xml) {
loadPublication(xml)
}
});
}
function loadPublication(xmlData) {
i = 0;
var row;
var searchExp = "";
var ppid = "P";
$(xmlData).find('PPID').each(function () {
if($(this).find('url').attr('address').length) {
var record = $(this).find('record').attr('search');
var archive = $(this).find('Archive');
RegExp.escape = function(keyword) {
return keyword.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
var exp = new RegExp(keyword, "i");
searchExp = record.match(exp);
if (searchExp != null) {
if ((i % 2) == 0) {
row = 'even';
} else {
row = 'odd';
}
if($(this).attr('PID') != ppid) {
ppid = $(this).attr('PID');
i++;
pub += '< tr id="wrapper" class="row ' + row + '">'
+ '< td colspan="8">'
+ '< a onclick="javascript:expgroupby(this);return false;" href="javascript:">'
+ '< img name="collapse" alt="expand" src="/_layouts/images/plus.gif" border="0" />< /a>'
+ '< a href="http://project.com/sites/tp/Projects/' + $(this).find('url').attr('address') + '">' + ' ' + $(this).attr('PID')+ ' - ' + $(this).attr('Description') + ' - ' + $(this).attr('Lead') + '< /a>< /td>'
+ '</tr>';
}
pub += '<tr id="item" style="display: none;">'
+ '< td valign="top" class="col2">' + $(this).find('WO').attr('WID') + '< /td>'
+ '< td valign="top" class="col3">' + $(this).find('WO').attr('Description') + '< /td>'
+ '< td valign="top" class="col4">' + $(this).find('WO').attr('PM') + '< /td>'
+ '< td valign="top" class="col5">' + $(this).find('WO').attr('Status') + '< /td>'
+ '< td valign="top" class="col6">' + $(this).find('WO').attr('WOXREF') + '< /td>'
+ '< td valign="top" class="col7">' + $(this).find('WO').attr('Program') + '< /td>'
+ '< td valign="top" class="col8">' + $(this).find('Archive').text() + '< /td>'
+ '< /tr>';
}
}
});
if (i == 0) {
pub += '< div class="error">';
pub += 'No results were found!';
pub += '< /div>';
$('#result').html(pub);
} else {
showResult(pub);
}
}
function showResult(resultSet) {
pub = '< div class="message">There are ' + i + ' results!< /div>';
pub += '< table id="grid" border="0">';
pub += '< thead>< tr>< td>< th class="mess">PPID - Project Description - Lead PM< /th>< /td>< /tr>';
pub += '< tr>< th class="col2">WO Number< /th>';
pub += '< th class="col3">WO Description< /th>';
pub += '< th class="col4">Project Manager< /th>';
pub += '< th class="col5">Status< /th>';
pub += '< th class="col6">XRef< /th>';
pub += '< th class="col7">Program< /th>';
pub += '< th class="col8">Archive Status< /th>';
pub += '< /tr>< /thead>';
pub += '< tbody>';
pub += resultSet;
pub += '< /tbody>';
pub += '< /table>';
$('#result').html(pub)
$('#grid').tablesorter();
}
});
function expgroupby(e) {
docElts=document.all;
numElts=docElts.length;
images = e.getElementsByTagName("IMG");
img=images[0];
srcPath=img.src;
index=srcPath.lastIndexOf("/");
imgName=srcPath.slice(index+1);
var b="auto";
if(imgName=="plus.gif"){
b="";
img.src="/_layouts/images/minus.gif"
}else{
b="none";
img.src="/_layouts/images/plus.gif"
}
oldName=img.name;
img.name=img.alt;
spanNode=img;
while(spanNode!=null){
spanNode=spanNode.parentNode;
if(spanNode!=null&&spanNode.id!=null&&spanNode.id=="wrapper")break
}
while(spanNode.nextSibling!=null&&spanNode.nextSibling.id!="wrapper"){
spanNode=spanNode.nextSibling;
spanNode.style.display=b;
}
}
History
ActiveX parsing may be added at a later date for additional cross-browser (IE 8) support.