Synook

An AJAX shoutbox

In this article I will walk you through making a simple but effective shoutbox, using PHP in the server-side, a MySQL database to store information and AJAX for smooth client interaction.

First, we will create the table that will hold the conversation history. Since in this shoutbox system we will not have users or any other complexities the table will be very simple, with four columns: the shout ID, the user, the time, and the actual shout.

CREATE TABLE shout (
  id INT(8) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(64) NOT NULL,
  datetime INT(32) NOT NULL,
  shout TEXT NOT NULL
);

Then, in a new HTML document, we will create the front-end interface for the shoutbox. Suffice to say it can be very simple, and in this tutorial we won’t be covering styling or anything. All that is needed is a division in which the shouts will be displayed through AJAX, and a form by which the client can submit their own shouts. Also there is a console division, to notify the client of errors.

<p id="shoutbox"><?php include("getshouts.php"); ?></p> 
<form action="" method="post" onsubmit="return push_shout()">
  <input type="text" name="user" id="user" /> 
  <input type="text" name="shout" id="shout" />
  <input type="submit" value="Shout" />
</form>
<p id="console"></p>

Now for the JavaScript and backend scripts. All we need are two script, one that will display the shouts and one that will accept user input. To reduce processing and bandwidth costs, however, we will have another very small script that just tells our front-end script whether there are any new posts, by returning the total number. So we end up with three PHP documents.

First is shouts.php, which basically just queries the database and echoes back the number of shouts.

<?php
  require_once("config.php");
  echo implode(mysql_fetch_assoc(mysql_query("SELECT COUNT(id) FROM shout")));
?>

Then, getshouts.php is called if the number of shouts has changed, and this sends back the shouts, already formatted for direct inclusion into our #shoutbox division. Note that the rows are reversed so that while we select the shouts in DESC order so we only get the last 10, we still end up with the latest one at the bottom.

<?php
  require_once("config.php");
  $result = mysql_query("SELECT * FROM shout ORDER BY id DESC LIMIT 10");
  $lines = array();
  while ($shout = mysql_fetch_assoc($result)) {
    $lines[] = "{$shout['username']} (" . date("H:i:s", $shout['datetime']) . ") :: {$shout['shout']}<br />";
  }
  echo implode(array_reverse($lines));
?>

And finally, shout.php, which receives POST data and adds that to the database. In this file, I have also included flood control so that people can’t repeatedly shout over and over again. The construction of the fcontrol() function and how it works can be found in my article Flood control in PHP. Also, don’t forget sanitization!

<?php
  require_once("config.php");
  if (fcontrol("shout", 5)) {
    $user = mysql_real_escape_string($_POST['user']); 
    $shout = mysql_real_escape_string($_POST['shout']); 
    $user = htmlspecialchars($user);
    $shout = htmlspecialchars($shout);
    mysql_query("INSERT INTO shout (username, datetime, shout) VALUES ('$user', " . time() . ", '$shout')");
  } else echo "Please wait...";
?>

You will notice that all these files require config.php. This is just a configuration file that connects to the database, and also defines fcontrol() for the benefit of shout.php.

Now for the front-end scripts, in JavaScript, which are responsible for requesting the backend PHP files. As follows, there are three main functions, each calling one page. We also have two "utility" functions that aid us – described first below. Their functions are fairly obvious – $() is just a shortcut for document.getElementById(), and urlencode() urlencodes a string (necessary for our POST call to shout.php).

  var current_shouts = 0;
  function $(eleid) {
    return document.getElementById(eleid);
  }
  function urlencode(u) {
    u = u.toString();
    var matches = u.match(/[\x90-\xFF]/g);
    if (matches) {
      for (var mid = 0; mid < matches.length; mid++) {
        var char_code = matches[mid].charCodeAt(0);
        u = u.replace(matches[mid], '%u00' + (char_code & 0xFF).toString(16).toUpperCase());
    } 
  } 
  return escape(u).replace(/\+/g, "%2B");
}

Our first function, shouts(), is the most commonly called one, and it just checks whether there are new shouts, by checking against our current_shouts variable defined above. If the numbers do not equate then it updates current_shouts and calls getshouts(). The setTimeout() call ensures that shouts() is called every second, so that other people’s shouts are printed more or less immediately. Update: to stop Internet Explorer caching shouts.php (and therefore making it look like no shouts are ever added), we just add a small querystring with a random number on the end to make it look like a different page. I find this method more reliable than header modification.

function shouts() {
  clearTimeout(getshout);
  var xmlHttp = (window.XMLHttpRequest) ? new XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP"); 
  xmlHttp.open("GET", "shouts.php?i=" + Math.random()); 
  xmlHttp.onreadystatechange = function() {
    if (this.readyState == 4) {
      if (parseInt(this.responseText) > current_shouts) {
        getshouts();
        current_shouts = parseInt(this.responseText);
      }
      getshout = setTimeout("shouts()", 1000);
    }
  } 
  xmlHttp.send(null);
}

The next function is getshouts(). This simply requests getshouts.php, and assigns the returned text to the #shoutbox division.

function getshouts() {
  var xmlHttp = (window.XMLHttpRequest) ? new XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP"); 
  xmlHttp.open("GET", "getshouts.php?i=" + Math.random()); 
  xmlHttp.onreadystatechange = function() {
    if (this.readyState == 4) $("shoutbox").innerHTML = this.responseText;
  } 
  xmlHttp.send(null); 
}

The code that sends the client’s shout to the server is split into two functions, because due to the asynchronicity of AJAX calling the XMLHttpRequest() directly from the submit handler of the form will cause it to submit before the function had a chance to cancel the action. Also, because we use the POST method to send the shout additional header assignments are needed. Notice also that the function is written so that if shout.php returns some text, it will be printed to the #console division.

function shout() {
  var xmlHttp = (window.XMLHttpRequest) ? new XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP"); 
  xmlHttp.open("POST", "shout.php");
  var data = "user=" + urlencode($("user").value) + "&" + "shout=" + urlencode($("shout").value); 
  xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  xmlHttp.setRequestHeader("Content-length", data.length); 
  xmlHttp.onreadystatechange = function() {
    if (this.readyState == 4) {
      if (!this.responseText) $("shout").value = "";
      else {
        $("console").innerHTML = this.responseText; 
        setTimeout("$('console').innerHTML = ''", 5000);
      }
      getshouts();
    }
  }
  xmlHttp.send(data);
  return true;
}

At the very end, we set the entire thing going with a timeout call to shouts().

var getshout = setTimeout("shouts()", 1000);

As a final note, we should make the application degrade gracefully when JavaScript is disabled. In our case, this is very simple. Our form is already set to submit to the same page in the case of a failure by shout(), so we just need to include() the server-side data handler at the top of our main shoutbox page, as such:

<?php
  if (isset($_POST['shout'])) {
    include("shout.php");
  }
?>

And, so that the shouts are displayed even without JS enabled, we print them into #shoutbox when the page loads, with PHP:

<p id="shoutbox"><?php include("getshouts.php"); ?></p>

We are finished!