AJAX Demythified for PHP Programmers

first published on 20/09/2006

1. Purpose

To simply and efficiently ***use*** AJAX on your PHP-driven websites, without having to plunge into theosophical arguments about XML, DTDs or frameworks.

2. Introduction

When it comes to AJAX, discussions can turn into things like this ERT Forum's topic because the proposed Article didn't mention "AJAX frameworks", "JSON and others alternatives", "Using data islands in conjunction with XMLDOM"...
Well, I don't give a s**t and will not enter this ranting discussion.

All I want is to be able to USE the so-called buzz/hype/must-do technique called AJAX, and to be able to do this SIMPLY for a very SIMPLE purpose : to be able to trigger client-side queries to the server, whilst using PHP on the server to generate the client's DHTML code. For instance to enable the user to update a tree menu's labels stored in a RDBMS on the server. Or to dynamically get from that very same RDBMS data based on the current selection on a listbox, triggered by OnChange() event. I did all of that using the hereafter-described technique.

You could indeed say that what I need is "only Remote Scripting", but to me and AFAIK, AJAX is nothing more than "XMLHttpRequest + Javascript" (QUOTE : "AJAX stands for Asynchronous JavaScript And XML" ). Call it "Remote Scripting" if you like. I personally don't care at all.


You can get a full description of AJAX in English (presents the AJAX basic use the same way as the above ERT Discussion) at http://www.javarss.com/ajax/j2ee-ajax.html and you can get a remarquable description of the XMLHttpRequest Object at http://openweb.eu.org/articles/objet_xmlhttprequest/ (in French, in Google cache if link stays down, and I made a local copy just in case)

3. What You Really Need to Use AJAX

  1. First of all, you need a wrapper around the XMLHttpRequest object, which covers the platform differences (ActiveX, DOM), which handles instanciation problems, enables you to simply say "GET URL x... with parameters..." and all of this without breaking your javascript flow and for the smallest footprint possible.
  2. Then, you need a return function (a bit like a callback function)
  3. And last, you need to code a very simple PHP script on the server to handle the queries



The first part does exist, I found it :D
It's called SACK ( Simple AJAX Code-Kit ) ©2005 Gregory Wild-Smith at http://www.twilightuniverse.com and it does it all for 4491 bytes ;-)

This is the file ajax.js (save as after opening link, or directly do Save Link As... and rename to .js :D )

JavaScript code:


/* Simple AJAX Code-Kit (SACK) */
/* ©2005 Gregory Wild-Smith */
/* www.twilightuniverse.com */
/* Software licenced under a modified X11 licence, see documentation or authors website for more details */

function sack(file){
  this.AjaxFailedAlert = "Your browser does not support the enhanced functionality of this website, and therefore you will have an experience that differs from the intended one.\n";
  this.requestFile = file;
  this.method = "POST";
  this.URLString = "";
  this.encodeURIString = true;
  this.execute = false;

  this.onLoading = function() { };
  this.onLoaded = function() { };
  this.onInteractive = function() { };
  this.onCompletion = function() { };

  this.createAJAX = function() {
    try {
      this.xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
      try {
        this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (err) {
        this.xmlhttp = null;
      }
    }
    if(!this.xmlhttp && typeof XMLHttpRequest != "undefined")
      this.xmlhttp = new XMLHttpRequest();
    if (!this.xmlhttp){
      this.failed = true; 
    }
  };
  
  this.setVar = function(name, value){
    if (this.URLString.length < 3){
      this.URLString = name + "=" + value;
    } else {
      this.URLString += "&" + name + "=" + value;
    }
  }
  
  this.encVar = function(name, value){
    var varString = encodeURIComponent(name) + "=" + encodeURIComponent(value);
  return varString;
  }
  
  this.encodeURLString = function(string){
    varArray = string.split('&');
    for (i = 0; i < varArray.length; i++){
      urlVars = varArray[i].split('=');
      if (urlVars[0].indexOf('amp;') != -1){
        urlVars[0] = urlVars[0].substring(4);
      }
      varArray[i] = this.encVar(urlVars[0],urlVars[1]);
    }
  return varArray.join('&');
  }
  
  this.runResponse = function(){
    eval(this.response);
  }
  
  this.runAJAX = function(urlstring){
    this.responseStatus = new Array(2);
    if(this.failed && this.AjaxFailedAlert){ 
      alert(this.AjaxFailedAlert); 
    } else {
      if (urlstring){ 
        if (this.URLString.length){
          this.URLString = this.URLString + "&" + urlstring; 
        } else {
          this.URLString = urlstring; 
        }
      }
      if (this.encodeURIString){
        var timeval = new Date().getTime(); 
        this.URLString = this.encodeURLString(this.URLString);
        this.setVar("rndval", timeval);
      }
      if (this.element) { this.elementObj = document.getElementById(this.element); }
      if (this.xmlhttp) {
        var self = this;
        if (this.method == "GET") {
          var totalurlstring = this.requestFile + "?" + this.URLString;
          this.xmlhttp.open(this.method, totalurlstring, true);
        } else {
          this.xmlhttp.open(this.method, this.requestFile, true);
        }
        if (this.method == "POST"){
            try {
            this.xmlhttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded')  
          } catch (e) {}
        }

        this.xmlhttp.send(this.URLString);
        this.xmlhttp.onreadystatechange = function() {
          switch (self.xmlhttp.readyState){
            case 1:
              self.onLoading();
            break;
            case 2:
              self.onLoaded();
            break;
            case 3:
              self.onInteractive();
            break;
            case 4:
              self.response = self.xmlhttp.responseText;
              self.responseXML = self.xmlhttp.responseXML;
              self.responseStatus[0] = self.xmlhttp.status;
              self.responseStatus[1] = self.xmlhttp.statusText;
              self.onCompletion();
              if(self.execute){ self.runResponse(); }
              if (self.elementObj) {
                var elemNodeName = self.elementObj.nodeName;
                elemNodeName.toLowerCase();
                if (elemNodeName == "input" || elemNodeName == "select" || elemNodeName == "option" || elemNodeName == "textarea"){
                  self.elementObj.value = self.response;
                } else {
                  self.elementObj.innerHTML = self.response;
                }
              }
              self.URLString = "";
            break;
          }
        };
      }
    }
  };
this.createAJAX();
}

var ajax = new sack(); //VGR26042006 ADDed declaration here

4. An Example Of Use

My PHP scripts would generate such a JavaScript Code :

DHTML HEAD code:


<script language="JavaScript" type="text/javascript" src="ajax.js"></script>

DHTML BODY code:



function showUpdate() {
  document.getElementById('ajaxMessage').innerHTML = ajax.response;
}

function hideEdit() {
  var editObj = editEl.previousSibling;
  if (editObj.value.length>0) {
    editEl.innerHTML = editObj.value;
    var parentid=editEl.getAttribute("id_parent");
    var extrastring=(parentid!=null)?"&id_parent="+parentid:"";

    ajax.requestFile = 'updateNode.php?updateNode='+editObj.id.replace(/[^0-9]/g,'') + '&newValue='+editObj.value + extrastring;
    ajax.onCompletion = showUpdate; // Specify function that will be executed after file has been found
    ajax.runAJAX(); // Execute AJAX function
  }
}

HTML Elements:

You must include such a call via DOM manipulation (like in here) or simply via a OnMouseDown="hideEdit();" directly on the HTML tag


anyTag.onmousedown = hideEdit;

PHP Elements :

This is the file referenced above, called 'updateNode.php'
This time, the PHP script just outputs data the its standard output (in this case, the client's browser) where the XMLHttprequest Object is listening


I also have cases when I output formatted data with separator, that I split on the receiver side.

<?php
//
// updateNode.php
//
//VGR06032006 Finished
//VGR19032006 ADDed sécurité contre les fausses insertions ('Nouvelle...' transmis)
//VGR01092006 MOD for publication on EEE.org ( www.europeanexperts.org ) and ERT (www.expertsrt.com | net )
//
// REM reçoit updateNode=2&newValue=dd en GET
//
// TODO : Nil
//

if ( isset($_GET['newValue']) AND isset($_GET['updateNode']) ) {
  require_once('header.inc.php'); //VGR01092006 defines DB settings etc
  $loclinkadd=txt('Nouvelle...'); //VGR19032006 ADDed : this is the default value contained on the client side
  $newValue=addslashes($_GET['newValue']);
  $updateNode=$_GET['updateNode'];
  $id_parent=(isset($_GET['id_parent']))?$_GET['id_parent']:$updateNode;
  if ($newValue<>$loclinkadd) { //VGR19032006 ADDed sécurité (else NOP)
    if ($updateNode>$iszero) { // new category
      $query="select max(id) AS a from $dbTableCategories;";
      $result=mysql_query($query,$linkId) or die("bad query '$query' : ".mysql_error());
      $res=mysql_fetch_array($result);
      $nextid=$res['a']+1;
      $command='insert into';
      if ($id_parent==0) { // new main category
        $extracmd=",id_parent=$nextid,id_l={$_SESSION['sess_langue']},id=$nextid";
      } else $extracmd=",id_parent=$id_parent,id_l={$_SESSION['sess_langue']},id=$nextid";
      $_GET['updateNode']=$nextid;
    } else { $command='update'; $extracmd=" where id=$updateNode AND id_l={$_SESSION['sess_langue']}"; } // normal update
    $query="$command $dbTableCategories set description='$newValue'{$extracmd};";
    $result=mysql_query($query,$linkId) or die("bad query '$query' : ".mysql_error());
    echo "update done ('$command') newval={$_GET['newValue']} pour id={$_GET['updateNode']}";
  } // else NOP
  else echo "NOP";
} else echo "appel incorrect.";
?>

5. An Other Example

My PHP scripts would generate such a JavaScript Code :

DHTML HEAD code:


<script language="JavaScript" type="text/javascript" src="ajax.js"></script>

HTML Elements :

This, if used, must be generated before the rest of the DHTML BODY Code


<div id="ajaxMessage"></div>

HTML Elements at the point of Use by the Client:

This time, it's directly on the HTML tag


echo " <img src=\"$ModFavIcon\" align=\"middle\" Onclick=\"EditFav({$res['id']});\" alt=\"Edit\" title=\"$loctxtModFav\" class=\"clickableimg\">";

DHTML BODY code:

This code can be generated at any point in the BODY (in my case, at the end)


<div id="fav_popup" class="opaque popup" OnKeyDown="defaultvalid(event);">
<script language="JavaScript" type="text/javascript">

function defaultvalid(event) {
  if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
    appliquer(false);
    return false;
  } else return false;
}

_dom=(document.all?3:(document.getElementById?1:(document.layers?2:0)));

function hide_fav_popup() {
  fav_popup_layer=(_dom!=3)?document.getElementById('fav_popup'):fav_popup;
  form=document.forms.addfav;
  document.getElementById("deleteButton").style.visibility = "hidden";
  document.getElementById("deleteButton").parentNode.disabled = "disabled";
  fav_popup_layer.style.visibility="hidden";
}

function appliquer(parDel) { //VGR01092006 REM this triggers a PHP script to perform the DB update/insert/delete
  form=document.forms.addfav;
  var thesaisie=form.formlink.value;
  var theref=escape(trim(thesaisie)); //VGR REM equivalent to urlencode()
  var thetitre=escape(form.formtitre.value);
  var thecomm=escape(form.formcomm.value);
  var thekeyw1=escape(form.formkeyw1.value);
  var thekeyw2=escape(form.formkeyw2.value);
  var thekeyw3=escape(form.formkeyw3.value);
  var theid=form.previd.value;
  fav_popup_layer=(_dom!=3)?document.getElementById('fav_popup'):fav_popup;
  fav_popup_layer.style.visibility="hidden";
  var delme=0;
  if ( (theref!=escape('DefaultValue'))&&(theref!='') ) {
    // data protection
    if (thetitre=='') thetitre=thesaisie.replace(/^http:\/\/www\.(.*)$/g,"$1");
    var thenewval=thesaisie.replace(/^http:\/\/www\.(.*)\.(com|org|net|fr|biz|info)$/g,"site de $1");
    if (parDel) delme=1;
    // apply
    parent.frames[1].location.href="real_index.php?previd="+theid+"&addfav="+theref+"&titre="+thetitre+"&comm="+thecomm+"&keyw="+thekeyw1+"|"+thekeyw2+"|"+thekeyw3+"&delme="+delme;
  } // else NOP
}

var curid=0;
function remplit() { //VGR01092006 REM this gets the data from the Server via AJAX
  var id=curid;
  form=document.forms.addfav;
  //for tests only : document.getElementById('ajaxMessage').innerHTML = ajax.response;
  var reponse=ajax.response;
  // on a reçu : res['favori'].'|'.res['titre'].'|'.res['commentaire'].'|'.res['motsclefs'];
  var bidule=reponse.split('|');
  form.formlink.value=bidule[0];
  form.formtitre.value=bidule[1];
  form.formcomm.value=bidule[2];
  var bidule2=bidule[3].split(' '); // first three words
  form.formkeyw1.value=bidule2[0];
  form.formkeyw2.value=(bidule2.length>1)?bidule2[1]:"";
  form.formkeyw3.value=(bidule2.length>2)?bidule2[2]:"";
  form.previd.value=id;
  // set the delete button
  document.getElementById("deleteButton").style.visibility = "visible";
  document.getElementById("deleteButton").parentNode.disabled = "";
  form.editval.value=1;
  // display
  topPosition = parseInt(document.body.scrollTop) + 10;
  fav_popup_layer=(_dom!=3)?document.getElementById('fav_popup'):fav_popup;
  fav_popup_layer.style.visibility = "visible";
  fav_popup_layer.style.cursor="text";
  document.forms.addfav.formtitre.focus();
}

function EditFav(id) {
  // get data via AJAX ;-)
  ajax.requestFile = 'getFavorite.php?id='+id;
  curid=id;
  ajax.onCompletion = remplit; // Specify function that will be executed after file has been found
  ajax.runAJAX();// Execute AJAX function
}

</script>

<form name="addfav" id="addfav" action="" method="GET">
<input type="hidden" name="editval" id="editval" value="0">
<input type="hidden" name="previd" id="previd" value="0">
<!--unused in fact-->
<table width="100%"><tbody><tr><td align="right" width="*"><center><b>$loctxtaddfav</b></center></td><td align="right" width="15">&nbsp;&nbsp;<a href="javascript:hide_fav_popup();"><img src="$courant/images/cross_icon.jpg" width="9" height="9" border="0" alt="X"></a></td></tr>
<tr><td align="right">{$loctxttitre}&nbsp;<input name="formtitre" id="formtitre" type="text" size="40" maxlength="80" value=""></td><td>&nbsp;</td></tr>
<tr><td align="right">{$loctxtfav}&nbsp;<input name="formlink" id="formlink" type="text" size="40" maxlength="400" value=""></td><td>&nbsp;</td></tr>
<tr><td align="right">{$loctxtcomm}&nbsp;<input name="formcomm" id="formcomm" type="text" size="40" maxlength="80" value=""></td><td>&nbsp;</td></tr>
<tr><td align="right">{$loctxtclefs}&nbsp;<input name="formkeyw1" id="formkeyw1" type="text" size="10" maxlength="20" value="">&nbsp;<input name="formkeyw2" id="formkeyw2" type="text" size="10" maxlength="20" value="">&nbsp;<input name="formkeyw3" id="formkeyw3" type="text" size="10" maxlength="20" value=""></td><td>&nbsp;</td></tr>
<tr><td colspan="2" align="right"><a href="javascript:appliquer(true);"><img src="$DelFavIcon" id="deleteButton" style="visibility:	hidden;" border="0" alt="D"></a>&nbsp;&nbsp;<a href="javascript:appliquer(false);"><img src="$courant/images/icon_ok.png" border="0" alt="V"></a></td></tr>
</tbody></table>
</form>
</div>

PHP Elements :

This is the file referenced above
This time, the PHP script outputs formatted data with a separator, that I split on the receiver side.


<?php
//
// getFavorite.php
//
//VGR13032006 Création pour Q ajax
//VGR18032006 FIXed missing CRITICAL log level
//VGR28032006 FIXed addslashes() problem
//VGR05042006 FIXed problème d'id_user=0,1 pour l'admin (SBL)
//
// REM reçoit id= en GET
//
// TODO : Nil
//

if ( isset($_GET['id']) ) {
  require_once('header.inc.php');
  $query="select * from $dbTableFavoris where id={$_GET['id']};";
  $result=mysql_query($query,$linkId) or die("bad query '$query' : ".mysql_error());
  if ($res=mysql_fetch_array($result)) {
    if ( ($res['id_user']==$_SESSION['sess_id'])OR($_SESSION['sess_admin']>0) ) { // ok
      echo $res['favori'].'|'.stripslashes($res['titre']).'|'.stripslashes($res['commentaire']).'|'.stripslashes($res['motsclefs']);
    } else LogAction("Erreur Critique : utilisateur {$_SESSION['sess_id']} essaie d'effacer un favori {$_GET['id']} ne lui appartenant pas!",CRITICAL);
  } else LogAction("Erreur Critique : utilisateur {$_SESSION['sess_id']} essaie d'effacer un favori {$_GET['id']} inexistant!",CRITICAL);
} else echo "appel incorrect.";
?>

6. Demonstration (Working Example)

Here is a complete set of working files (that is, TWO files, the strict minimum minimorum) that you can see in action here.

Client HTML Demo page

Client side : AjaxDemo.php


<?php
//
// AjaxDemo.php : AJAX Demonstration Page
//
//VGR03092006 Creation
//
// TODO : Nil
//

echo <<<EOHEAD
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
<meta name="GENERATOR" content="Notepad">
<title>AJAX Demo</title>

<!-- this AJAX definition is usually best placed in HEAD-->
<script language="JavaScript" type="text/javascript" src="ajax.js"></script>
EOHEAD;

echo <<<EOHTML
</head>
<body>

<!-- this is the User Interface on which I "needed" AJAX to go to the server asynchronously-->
<form action="" method="POST" OnSubmit="return false;">
Input some ID : <input type="text" name="id" id="id_id" value="" OnChange="EditFunc();"> Dummy field to trigger OnChange : <input type="text" name="whatever" value="">
</form>
<hr>

<!-- this is not necessary ; for Demo purposes, it displays the AJAX answer.-->
<!--the style should normally be in the CSS file-->
AJAX Response : <div id="ajaxMessage" style="color:red;"></div>

<!-- this AJAX handling part should be in BODY, AFTER the referenced HTML elements-->
<script language="JavaScript" type="text/javascript">
function showUpdate() {
  document.getElementById('ajaxMessage').innerHTML = ajax.response;
}

var EditObj=document.getElementById('id_id');

function EditFunc() {
  var id=EditObj.value; if (id.length==0) id=null; // (1)
  var transmit_only_not_null=(id!=null)?"?id="+id:""; // (1)
  // get data via AJAX ;-)
  ajax.requestFile = 'getDummy.php' + transmit_only_not_null; // (1)
  ajax.onCompletion = showUpdate; // Specify function that will be executed after file has been found
  ajax.runAJAX();// Execute AJAX function
}

</script>

</body>
</html>
EOHTML;
?>

(1) For Demo purposes, I twisted the rather classical line ajax.requestFile = 'getDummy.php?id='+id; so that the id= parameter doesn't get transmitted via GET when no value is provided, to effectively trigger the "KO incorrect call" case on the server.

Server Script

Server side : this is getDummy.php, the file referenced above in the AJAX call
As you can see, you can play with entering '' (empty, void, null, 0-length string as you like), ' ' (spaces), 'qsd' ( a string value), 0 (zero) and non-zero numbers/scalars. The server will answer accordingly.


<?php
//
// getDummy.php
//
//VGR03092006 Creation for AJAX demonstration
//
// REM reçoit id= en GET (1) MAY receive
//
// TODO : Nil
//

if ( isset($_GET['id']) ) {
  if (is_numeric($_GET['id'])) {
    if ($_GET['id']>0) {
        echo "OK, received ID='{$_GET['id']}'";
    } else echo "KO, received ID=0";
  } else echo "KO, received non-scalar value '{$_GET['id']}'";
} else echo "KO, incorrect call";
?>

9. Conclusion

This technique is extremely simple and efficient and does the job well. You don't have to dig into unnecessary details (remember Occam's Razor).

Just include the correct .js file, drop the few necessary lines of JavaScript to your generated HTML pages, create one receiver script on the server, and you're done.


Best regards,
Vincent Graux (VGR) for European Experts Exchange and Experts Round Table and Edaìn Works  back to list of articles
Last update 2011-05-08 10:09:40