Wednesday, November 19, 2008

Information radiator part #2 - Javascript design + AJAX = CRAWLER!

So now that it's clear how to actually realize or classes, we can step back once and dig ourselves deeper into the design (again).
What I want is to share one single XMLHttp object over multiple requests. That is, we need to configure the object request with the correct (request, eventstateready function) for every request.
So we will need a way to couple the XmlHttp object with a readystate handler, for an example :
XMLHttpFactory.prototype.setReadyStateChange = function (onReadyStateChange) {
this.__xmlObj.onreadystatechange = onReadyStateChange;
}
Another thing to consider is the request itself : different requests require fetching of different server or static HTML pages for working (which will generate the result, this is a basic part of the AJAX technology which is not mentioned often enough as it should imo).
For sending a request to the Server, the XMLHttp object provides a basic "open" method for preparing the request, as well as a specific "send" method for forwarding it to the server. For an
example :
xmlHttp.open("GET","time.asp",true);
xmlHttp.send(null);
So let's add a specific method "submitRequest" in our Javascript prototype:
XMLHttpFactory.prototype.submitRequest = function (nature, target, onReadyStateChange) {
this.__xmlObj.open (nature, target, onReadyStateChange;
this.__xmlObj.send (null);
}
Our "submitRequest" method expects the kind of submitting (nature, "POST" or "GET"), the target server page, and the onReadyStateChange event handler. Notice that this makes the onreadyStateChange method we defined earlier unneeded. Thus I removed it.

Next is a class for managing the sequence configuration file (sequence & timings logic) and the configuration itself.

The configuration is represented in xml format to increase readability and understandability.
The various "display" tags represent a the single page that will be displayed. In this case "test1.php" will be fetched and displayed for 10 seconds and then it will be "test2.php"'s turn.

Considering the fact that we want to actually show webpages from different sources, we will first need to fetch them and then process them. Following this mindmap, we can come up with a straightforward structure :

  • on the bottom, a transport level which takes care of the XMLHttp handlign
  • on top of it, a presentation level, which displays pages based on the configuration file we feed it with
Easy.
The transport level is very basic. It's interface includes :
  1. a target URL to fetch
  2. a target element to insert the fetched webpage into
What I've come up with is (the code is pretty much self-explanatory, so I won't get more into detail):

//Class prototype for the XMLHttp transport facility.
function XMLHttpTransport(){
try
{
// Firefox, Opera 8.0+, Safari
this.__xmlObj = new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
this.__xmlObj = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
this.__xmlObj = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
return false;
}
}
}
}

//Variable for XMLHTTP Object
XMLHttpTransport.prototype.__xmlObj;
//Variable for Item Id - attached to xmlObj!
XMLHttpTransport.prototype.__item;


//Request submitter
XMLHttpTransport.prototype.submitRequest = function (nature, target, item) {
this.__xmlObj.__item = item;
this.__xmlObj.__target = target;
this.__xmlObj.onreadystatechange = this.ReadyStateChange;

try {
this.__xmlObj.open (nature, target, true);
}
catch (e) {
alert ("XMLHTTP Open Didn't work as expected! Exception : " + e.description);
}

try {
this.__xmlObj.send (null);
}
catch (e) {
alert ("XMLHTTP Send Didn't work as expected! Exception : " + e.description);
}
}

//Static event handler for basic display - assigns an element its inner html! - xmlHttpTransport is used as reference to the xmlhttp object. the context of invocation is from the xmlhttpobject, not xmlHttpTransport
XMLHttpTransport.prototype.ReadyStateChange = function (){
switch (xmlHttpTransport.__xmlObj.readyState)
{
case 0:
elText = "";
break;
case 1:
elText = "Request ready";
break;
case 2:
elText = "Request sent";
break;
case 3:
elText = "Processing request..";
break;
case 4:
if (xmlHttpTransport.__xmlObj.status == 200)
elText = xmlHttpTransport.__xmlObj.responseText;
else
elText = "Page Not Found: " + this.__target;
break;

}
document.getElementById(this.__item).innerHTML = elText;
}

While for the presentation level we will have :

//-----------------------------------------------------------------------------------------------------
//Timed execution
function SequenceConfig(configData){
//Loads the document
try
{
//All other browsers
parser = new DOMParser();
this._xmlDoc = parser.parseFromString(configData, "text/xml");
}
catch (e)
{
try
{
this._xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
this._xmlDoc.async = "false";
this._xmlDoc.loadXML(configData);
}
catch (e)
{
alert('No support for DOM in your browser?');
}
}
this._currentDisplay = 0;
}

//Configuration properties
SequenceConfig.prototype._xmlDoc;
SequenceConfig.prototype._currentDisplay;

SequenceConfig.prototype.displayNextPage = function (xmlHttpTransport){
//Initialize if needed

if (this._currentDisplay == -1)
{
el = this._xmlDoc.documentElement.firstChild;
this._currentDisplay = this._get_nextNode (el);
}
//time next display!
t = setTimeout("config.displayNextPage (xmlHttpTransport)", this._currentDisplay.getAttribute ('time') * 1000);
//Display it!
xmlHttpTransport.submitRequest ("GET", this._currentDisplay.getAttribute ('source'), "root");
//get next page
this._currentDisplay = this._currentDisplay = this._get_nextNode (this._currentDisplay.nextSibling);
}

SequenceConfig.prototype._get_nextNode = function (node){
x = node;
if (x == undefined)
{
return -1; //Stop if there are no more nodes
}

while (x.nodeType != 1)
{
x = x.nextSibling();
alert (x);
if (x == undefined)
{
return -1; //Stop if there are no more nodes
}
}
return x;
}

The configuration file is loaded into a dedicated variable at statup with help of some PHP code snipped (which loads the content of the configuration file and removes the newlines) :


//Configuration data will be included through a server-side include directly to the variable.
var configData = '';
var xmlHttpTransport = "";
var config;
So all what is missing now is a Javascript initiator (a method to start the whole process), which will be executed when the page loads (in the onLoad event of the "body" tag).
function init()
{
xmlHttpTransport = new XMLHttpTransport();
config = new SequenceConfig (configData);
config._currentDisplay = -1;
config.displayNextPage (xmlHttpTransport);
}
Easy eh? ;)

No comments: