Dynamic Site Navigation with AJAX

Recently a navigation challenge that I encountered was “better” resolved by implementing an AJAX “like” solution. While not a complete AJAX solution, this works by requesting and inserting HTML into the DOM dynamically. The HTML could even be the same content that is used when the page was initially generated allowing for the initial page render to be static HTML, and only the options to be dynamic.

This is an extension on my previous ‘Flexible AJAX Framework’ entry and adds the following features and methods:

  • Two column example page layout.
  • Page level caching of AJAX Responses.
  • JavaScript methods:
    • var pageLoadTime; – variable for page level caching.
    • function ajaxObjPageCache(obj,url,async,callback) – method for page level caching.
    • function menuAjaxHook(obj,customObj); – response handler that updates UI.
    • function xinnerHTML(id,txt); – this is a common component used by the example.
    • function testAjaxMenu(obj,menu); – test method for example.

The full example (in PHP) follows:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>AJAX Menu Example</title>
<script type="text/javascript">
var xbusy = true;
var xhr = null;
var otherHost = "http://example.giantgeek.com"; // do not use training slash!
var pageLoadTime = xmillis();// set this once per page for some caching solutions!
/**
* Check for existance on DOM browsers (Mozilla, etc.)
* @return xhr
*/
function ajaxCheckDOM(){
var myxhr = null;
if(window.XMLHttpRequest) {
try {
myxhr = new XMLHttpRequest();
}
catch(e) {}
}
return myxhr;
}
/**
* Check for existance on Windows/MSIE (prior to MSIE7 which is now DOM)
* Evaluate using - new ActiveXObject("Microsoft.XMLDOM");
* @return xhr
*/
function ajaxCheckActiveX(){
var myxhr = null;
//if(window.ActiveXObject){
try {
myxhr = new ActiveXObject("Msxml2.XMLHTTP");
}
catch(e) {
try {
myxhr = new ActiveXObject("Microsoft.XMLHTTP");
}
catch(e) {}
}
//}
return myxhr;
}
/**
* This is a default response hook for AJAX, you SHOULD create your own as it's only for DEMO
* @param obj - the clicked item
* @param customObj - user defined
*/
function ajaxResponseHook(obj,customObj){
var txt = xhr.responseText;// NOTE: xhr.responseXML is also valid
popStatus('AJAX Response value [' + txt + '|' + customObj +']','');
}
/**
* This is a common Response handler AJAX, you SHOULD NOT need to modify, use a 'custom' ajaxhook function to process the responses.
* @param obj - the clicked item
* @param ajaxhook Function (default will be used if undefined)
* @param customObj (optional - left for developer implementation, passed to the ajaxhook)
*/
function ajaxResponse(obj,ajaxhook,customObj){
var status='';
if(xhr!=undefined){
var state=xhr.readyState;
if(state!=undefined){
if(state==4){
var code = xhr.status;
status = xhr.statusText;
if(code==200){
popStatus('AJAX Response Received.','');
if(ajaxhook==undefined){
ajaxhook = function(){ ajaxResponseHook(obj,customObj); }
}
ajaxhook(obj,customObj);
ajaxBusy(obj,false);
} else {
popStatus('AJAX Error ' + code + ' ' + status + '.','');
}
ajaxBusy(obj,false);
xhr=null; /* MSIE6 leak fix */
}
}
}
}
/**
* NOTE: MSIE6-7 supports XSS url's (be careful!)
* @param obj - the clicked item
* @param url - the GET url params (FQDN)
* @param async (true or false) - false will LOCK browser until response - use cautiously!
* @param callback Function - allows for customized response handling
*/
function ajaxObj(obj,url,async,callback){
if(xbusy == true){
popStatus('AJAX BUSY, Please Retry.','');
} else {
if(callback==undefined){
callback = function(){ ajaxResponse(obj,hook,''); }
}
ajaxInit(obj,url,async,callback);
}
}
/**
* This is a Non-Caching implementation of ajaxObj() to show how you can avoid the MSIE caching issue.
* NOTE: You can use similar approaches to cache per page or session.
* @param obj - the clicked item
* @param url - the GET url params (FQDN)
* @param async (true or false) - false will LOCK browser until response - use cautiously!
* @param callback Function - allows for customized response handling
*/
function ajaxObjNoCache(obj,url,async,callback){
var cacheBuster=uniqueUrl(url);// damn MSIE!
ajaxObj(obj,cacheBuster,async,callback)
}
/**
* This is a Page-Caching implementation of ajaxObj() to allow for requests to be cached per pageload.
* @param obj - the clicked item
* @param url - the GET url params (FQDN)
* @param async (true or false) - false will LOCK browser until response - use cautiously!
* @param callback Function - allows for customized response handling
*/
function ajaxObjPageCache(obj,url,async,callback){
var cacheBuster=urlAppender(url,'.cache',pageLoadTime); // pageLoadTime should remain unchanged for page life.
ajaxObj(obj,cacheBuster,async,callback)
}
/**
* Initializes the AJAX operation
* @param obj - item clicked
* @param url - FQDN
* @param async (true/false) - for locking
* @param callback Function - provided for customization.
*/
function ajaxInit(obj,url,async,callback){
xhr=ajaxCheckDOM();
if(!xhr){
xhr=ajaxCheckActiveX();
}
if(xhr){
ajaxBusy(obj,true);
popStatus('AJAX Start.','');
xhr.onreadystatechange = callback; // method call
try {
xhr.open('GET',url,async); /* POST */
//xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.send(''); /* null */
}
catch(e)
{
if(xhr){
var code = xhr.status;
var status = xhr.statusText;
popStatus('AJAX Client SECURITY ' + navigator.appName +' '+ navigator.appVersion + ' ' + code + ' ' + status + '.','');
}
}
ajaxBusy(obj,false);
} else {
popStatus('AJAX Client ERROR.','');
}
}
function ajaxBusy(obj,yn){
if(yn){
swapStyleObj(obj,'idle','busy');
} else {
swapStyleObj(obj,'busy','idle');
}
xbusyInd(yn,'');
}
function xmillis(){
return new Date().getTime();
}
function swapStyleObj(obj,oldCSS,newCSS){
if(obj!=undefined) {
var current=obj.className;
if(current!=undefined){
var txtOld = current.replace(newCSS,' ');//no doubles
var txtMid = txtOld.replace(oldCSS, ' ');
var txtNew = (txtMid + ' ' + newCSS);
obj.className = txtNew;
} else {
obj.className = newCSS;
}
}
}
function xbusyInd(valnew, msg){
xbusy = valnew;
if(xbusy==true){
document.body.style.cursor='wait';
if(msg != ''){
top.window.defaultStatus=msg;
popStatus(msg,'');
}
showDiv('throbber');
//blockScreen();
} else {
document.body.style.cursor='default';
hideDiv('throbber');
//unBlockScreen();
}
}
function showDiv(id) { //show a div
var obj=xgetHelper(id);
if(obj!=null){ obj.style.display="block";}
//xrestart();
}
function hideDiv(id) { //hide a div
var obj=xgetHelper(id);
if(obj!=null){ obj.style.display="none";}
//xrestart();
}
function popStatus(txt,title){
popit('statusdyn','statusarrow','statusul','statusdiv',txt,false,title,'');
}
function popit(dynId,arrowId,ulId,divId,txt,expand,title,cssCls){
popText(ulId,txt,title,cssCls);
showDiv(divId);
if(expand == true){
var arrowObj=xgetHelper(arrowId); if(arrowObj!=null){ arrowObj.style.backgroundPosition='2px -106px'; }
showDiv(dynId);
}
}
function popText(id,txt,title,cssCls){
var obj = xgetHelper(id);
if(obj != null){
var oldHTML = obj.innerHTML;
var cls=''; if(cssCls!=''){ cls=' class="' + cls +'"'; }
var htm = '<'+'li'+ cls +'>' + txt + '<'+'/'+'li'+'>' + oldHTML;
obj.innerHTML = htm;
}
}
function xgetHelper(id){
var obj = null;
try {
obj = document.getElementById(id);
} catch(z) {
var dummy=alert("Error:" + z);
}
return obj;
}
function arrowTog(objectID,arrow) {
var obj = xgetHelper(objectID);
if(obj!=null){
if (obj.style.display =='block') {
arrow.style.backgroundPosition = '2px -21px';}
else {arrow.style.backgroundPosition = '2px -106px';}
objTog(objectID);
}
return false;
}
function objTog(objectID) {
var obj = xgetHelper(objectID);
if(obj!=null){
if (obj.style.display =='block') obj.style.display='none';
else {obj.style.display='block';}
}
return false;
}
function headTog(objectID,arrow) {
var obj = xgetHelper(objectID);
if(obj!=null){
if (obj.style.display =='block') {
arrow.style.borderBottomWidth = '1px';}
else {arrow.style.borderBottomWidth = '0px';}
arrowTog(objectID,arrow);
}
return false;
}
/*
* adds timestamp to URLs to make them unique
* @param URL String
*/
function uniqueUrl(x){
return urlAppender(x,'.cache',xmillis());
}
/*
* helps to add parms to the url
* @param URL String
* @param aname String
* @param avalue String
*/
function urlAppender(x,aname,avalue){
var delim = "?";
if(x.indexOf("?") >=0) { delim = "&"; }
return x + delim + aname + '=' + avalue;
}
function xload(){
hideDiv('throbber');
xbusy=false;
}
function testAjax(obj){
var callback=function(){ ajaxResponse(obj,null,'testAjax'); }
var x = ajaxObjNoCache(obj,'/ajax.php',true,callback);
}
function testAjaxParms(obj){
var callback=function(){ ajaxResponse(obj,null,'testAjaxParms'); }
var x = ajaxObjNoCache(obj,'/ajax.php?testing=Y',true,callback);
}
function testAjaxXSS(obj){
var callback=function(){ ajaxResponse(obj,null,'testAjaxXSS'); }
var x = ajaxObjNoCache(obj,otherHost+'/ajax.php',true,callback,otherHost);
}
function testAjaxHook(obj){
var hook=function(){ customAjaxHook(obj,'ajaxhookTestMessageObect'); }
var callback=function(){ ajaxResponse(obj,hook,'testAjaxHook'); }
var x = ajaxObjNoCache(obj,'/ajax.php',true,callback);
}
function testAjaxHookXSS(obj){
var hook=function(){ customAjaxHook(obj,'ajaxhookXSSMessageObect'); }
var callback=function(){ ajaxResponse(obj,hook,'testAjaxHookXSS'); }
var x = ajaxObjNoCache(obj,otherHost+'/ajax.php',true,callback);
}
function testAjaxMenu(obj,menu){
var mms_start=xmillis();
var hook=function(){ menuAjaxHook(obj,mms_start); }
var callback=function(){ ajaxResponse(obj,hook,'testAjaxHookXSS'); }
var x = ajaxObjPageCache(obj,'/ajaxmenu.php?menu='+menu,true,callback);
}
/**
* This is a custom implemenation, the customObj COULD be used for anything (perhaps delay measurement!)
* @param obj - the clicked item
* @param customObj - user defined
*/
function menuAjaxHook(obj,customObj){
var diff = xmillis() - customObj; // difference from click to now!
popStatus('AJAX Menu Response time [' + diff +'mms]','');
var htm = xhr.responseText;// NOTE: xhr.responseXMLis also valid
xinnerHTML('menu',htm);
}
/**
* This is a custom implemenation, the customObj COULD be used for anything (perhaps delay measurement!)
* @param obj - the clicked item
* @param customObj - user defined
*/
function customAjaxHook(obj,customObj){
var xml = xhr.responseXML;// NOTE: xhr.responseText is also valid
var tmp = 'DEMO customAjaxHook [' + customObj + ']\n' + xml;
alert(tmp);
}
function xinnerHTML(id,txt){
var obj = xgetHelper(id);
if(obj!=null){
if((txt != null) && (txt != "")){
obj.innerHTML = txt;
}
}
}
</script>
<style type="text/css">
div#container {position:absolute;top:0;left:0;padding-right:10px;}
div#nav_col {position:absolute;left:0;
float:left;
width:140px;padding-left:7px;
}
div#container > div#nav_col {position:relative;}

div#main {margin-left:160px;border:1px dotted #fff;/*vertical-align:top; causes table gaps bug*/
width:99.9%;
voice-family: “\”}\””;
voice-family:inherit;
width:auto;}
.busy { color:red; }
.idle { color:black; }
</style>
</head>
<body onload=”xload();”><!– onbeforeunload=”alert(‘before’);” onunload=”alert(‘after’);” –>
<div id=”throbber”>WORKING!</div>
<div id=”container”>
<div id=”nav_col”>
<div id=”menu”>EMPTY Menu</div>
</div>
<div id=”main”>
<div id=”statusdiv” class=”dyn” style=”display:none;”>
<h3><a id=”statusarrow” onclick=”headTog(‘statusdyn’,this);” href=”javascript:void(0);”>Status</a></h3>
<fieldset id=”statusdyn” style=”display:block;background-color:#ffffcc;”>
<div id=”statusBox” class=”box”>
<ul id=”statusul” class=”error”>
<li id=”ajaxStatus” style=”list-style:none;display:none;”>FILLER</li>
</ul>
</div>
</fieldset>
</div>
<h3><a href=”javascript:void(0);” onclick=”testAjax(this);”>TEST</a></h3>
<h3><a href=”javascript:void(0);” onclick=”testAjaxParms(this);”>TESTPARMS</a></h3>
<h3><a href=”javascript:void(0);” onclick=”testAjaxXSS(this);”>TESTXSS</a></h3>
<h3><a href=”javascript:void(0);” onclick=”testAjaxHook(this);”>TESTHOOK</a></h3>
<h3><a href=”javascript:void(0);” onclick=”testAjaxHookXSS(this);”>TESTHOOKXSS</a></h3>
<h3><a href=”javascript:void(0);” onclick=”testAjaxMenu(this,’ACCT’);”>TESTMenuACCT</a></h3>
<h3><a href=”javascript:void(0);” onclick=”testAjaxMenu(this,’USER’);”>TESTMenuUSER</a></h3>
<p><a href=”index.php”>RELOAD</a></p>
</div><!– main –>
</div><!– container –>
</body>
</html>

ajax.php – for the original test code:

<?php
//header("Cache-Control: no-store");
header("Charset: UTF-8");
header("Content-Type: text/xml");
echo("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
?>
<test><?php echo( gmdate("D, d M Y H:i:s") ) ?> </test>

ajaxmenu.php – example menu behavior:

<?php
//header("Cache-Control: no-store");
header("Charset: UTF-8");
header("Content-Type: text/html");
?>
<ul>
<li>MENU:</li>
<li><?php echo($_GET['menu']) ?> </li>
<li><?php echo( gmdate("D, d M Y H:i:s") ) ?> </li>
</ul>

Cheers!