HOME FORUM ARTICLES TUTORIALS SCRIPTS LINKS NEWS MENTORS TOOLS REGISTER

AJAX Demythified for PHP Programmers

An article by ERT Mentor VGR

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 as far as I am aware, 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 remarkable description of the XMLHttpRequest Object at http://openweb.eu.org/articles/objet_xmlhttprequest/ in French.

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 instantiation 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 ;-)

You may cut and paste the code below into a file and name it: ajax.js
Or you may just click on this link to get the file ajax.js (save as after opening link, or directly do Save Link As... and rename to .js :D )
In any case, Please leave the copyright notice intact whichever use you do of the file. This is an ethical issue.

JavaScript code:


/* Simple AJAX Code-Kit (SACK) */
/* ©2005 Gregory Wild-Smith */
/* www.twilightuniverse.com */
/* Software licensed under a modified X11 license, 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 to 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. For this file and all the .php files referenced here, you will need to change the file extension from .txt to .php before using them.

<?php
//
// updateNode.php
//
//VGR06032006 Finished
//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 client value 
  $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.";
?&gt;

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']});\"";
echo " 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";
}

//VGR01092006 this triggers a PHP script to perform the DB update/insert/delete
//
function appliquer(parDel) { 
  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
//
// 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 
        // favorite does not belong to user
  } // else favorite doesn't exist
} 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";
?>

7. 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.

© Copyright VGR 2006-2007

post to Dzone Digg this! Add to del.icio.us Googleize this Add to reddit Save to myYahoo Add to furl Add to Netvouz! Spurl this! Add to Linkroll! Save to Simpy Give if thumbs up on StumbleUpon Save to Blinklist Add to Tektag Save to Bibsonomy Submit to Tweako
Search ERT on the Tools Page

Did you know? You can discuss this article with the mentor who wrote it and others interested in the topic? You are invited to join the discussion with Go to the forum

Got a technical article or tutorial you want to publish on the Internet? Join Go to the forum in the Round Table Forum and let the Mentors know what you have. If it meets ERT standards, is factual and can help ERT visitors, then ERT Mentors and Editors can help you (without charge) polish your offering so it can be published and promoted by ERT. An article published on ERT may be read by as many as 10,000 visitors a week; promoting you, your site, and your ideas. Please note ERT does not publish re-prints; promotional handouts, or pieces consisting mainly of links. So original technical content only please. If you prefer you can email the Editor