Dirk Bertels

The greatest malfunction of spirit
is to believe things (Louis Pasteur)

Asynchronous Commenting with Ajax

Last updated 09 September 2014 (see same-dated comment at bottom)

Index

Introduction
The downside of using asynchronicity
The MySql Database file and methods
Integration of commenting system into web pages
Implementing the Ajax functionality in the template file
Implementing the Server functionality
Trying out the commenting system

Introduction

This is a follow-up from my first article on ajax, Using Ajax with PHP. It fully describes the asynchronous commenting system I use on this website - and discusses all the code to implement it. The code uses mysql for the database, php for the server, and Javascript with Ajax on the client side. The system's keypoints are:

  • Asynchronous transport mechanism
  • Easy integration within any web page
  • Handles multiple pages
  • Local validation of the form fields
  • 'secret code' to avoid spam, checked on the server
  • email notification of new messages

This implementation makes use of two extra javascript libraries which are very useful:

I use these 2 libraries because they handle the not-so-sexy parts in the implementation. You can easily bypass them and implement all this yourself - but you'll need to be one heck of a programmer to do a better job. Also, these libraries allow me to concentrate on those parts in the code that matter, and hence describe things with much more clarity.

The code we will discuss can be adapted to suit many uses. After all, the commenting and emailing functionality is more of a sideline when considered from an Ajax viewpoint. What concerns Ajax is how to implement the form and how to handle the reply. So in that sense, what PHP does is of no consequence to Ajax as long as it gets some sort of response. Ajax is good at communicating with the server - and what it communicates is largely dependent on the implementation - hence its universality.

Go to the commenting system at the end of this page and play around with 'wrong values'. If any of the form elements are red or if you enter the wrong code number, you can send the email and watch the response. You will notice 3 areas where you get feedback:

  • Red comments behind the form elements are the result of the livevalidation script. As long as any remain red, you can try sending the form data but it will not be sent. The div area called ajaxReply (see below) will display a message from Ajax in red.
  • If you enter the wrong code number, and all elements are green, then Ajax will display the server response in a div area named commentsLocation. This location holds all the commenting data plus the feedback (in red) from the server. It will appear above the form in the center.
  • Then there is the div area, named ajaxReply in which replies from ajax are displayed. This text, also in red and located above the form, appears on the left side.

back to index

The downside of using asynchronicity

There is one disadvantage, which is the one often quoted with any Ajax implementation: Since the page is not reloaded, the Back button on the browser doesn't recognise this transaction. This may seem trivial, but it isn't when you consider that the Back button is statistically the button that users use most of all in a browser. Mind you, for me personally, I often like this attribute - When I want to go back, I want to go back to a major state, not repeatedly pass trivial states. So I think if Ajax gets used more widely, then people will intuitively recognise which states are asynchronous and which ones aren't. Moreover, there are implementations which can take care of this: While the internal history mechanism built into a web browser can't be tampered with for security reasons, they solve the problem by creating a history stack and use cookies to remember the states.

back to index

The Mysql Database file and methods

		
DROP TABLE IF EXISTS topics;
CREATE TABLE topics
(
  topicId 	   INTEGER NOT NULL AUTO_INCREMENT,
  name		   VARCHAR(128) NOT NULL,
  url		   varchar(255) NOT NULL,
  PRIMARY KEY (topicId)
);

DROP TABLE IF EXISTS comments;
CREATE TABLE comments
(
  commentId	   INTEGER NOT NULL AUTO_INCREMENT,
  postDate	   DATETIME NOT NULL,
  topicId	   INTEGER NOT NULL,
  email		   VARCHAR(255) NOT NULL,
  name		   VARCHAR(255) NOT NULL,
  commentText  TEXT NOT NULL,
  PRIMARY KEY (commentId)
);

The topics table represents the context that the commenting form is used in. It essentially describes the page that the form resides in. For example, the forum at the end of this page has the following values in the topics table:

  • 3
  • Asynchronous Commenting with Ajax
  • http://www.dirkbertels.net/computing/ajax2.php

The comments table entries represent the attributes that each comment possesses - these comprise of the user data [name, email, commentText] and management data [commentId, topicId, postDate]

This project uses standard PHP/Mysql commands. Initially, PEAR was used for convenience sake, but a few strange behaviours made me switch to what I know works and is not so dependent on what extensions are installed on the server.

I also have 2 php files that handles very basic database functions:


<?
function db_connect()
{
   $result = mysql_connect("localhost", "[yourUsername]", "[yourPassword]"); 

   if (!$result)
   {
      echo "Error: Could not connect to the database...";
      return false;
   }
   if (!mysql_select_db("[databaseName]"))
   {
      echo "Error: Could not select the database...";      
      return false;
   }

   return $result;
}
?>

The 'localhost' entry should work because php accesses the database locally. Your username and password refer to the database username and password, and the database name refers to the name of the database that holds the 2 tables.


<?
function clean($string)
{
  $string = trim($string);
  $string = htmlentities($string);
  $string = strip_tags($string);
  //$string = addslashes($string);			
  return $string;
}

function clean_all($form_vars)
{
  foreach ($form_vars as $key => $value)
  {
     $form_vars[$key] = clean($value);
  }
  return $form_vars;
}
?>

This file originally has more functions but these are the only one we are using here.
Obviously we could have these 2 php files in one file or even integrate them into the main file we will discuss soon. The reason I have these as separate files is becauses I use them for the have your say forum as well.

back to index

Integration of commenting system into web pages

We can integrate this forum into as many pages as we want by using just 2 lines. The first one sets the id of the particular page (the topic), the second one includes the template file which we will discuss soon. There is one condition though, and that is that all these files should reside on the same level relative to the template file.
This website, for example, contains many pages but they all are one level away from the top index file. This seems to work ok, especially since I have a tree navigation system that keeps things ordered for both me and the user.

The following code needs to be integrated into each page at the location where the commenting system will reside (typically at the end of the page):


<!-- PREPARE FOR COMMENTS FORM -->
<?php $topic = '2' ?>

<!-- START COMMENTING SECTION -->
<?php include ('../commentingTemplate.php'); ?>
<!-- END COMMENTING SECTION -->

The topic variable is different for each page of course. I set all the details of that particular page (or section) in the Topics table manually, an example of which I have given at the beginning of the database discussion.

back to index

Implementing Ajax functionality in the template file

The template file is the file where all the Ajax action happens. It needs to be included into each page or section that requires a commenting forum. First look at the code:



<script src="../prototype.js" type="text/javascript"></script>
<script src="../livevalidation.js" type="text/javascript"></script>
<br />
<div class="entry">
    <h3 class="entrytitle">Comments</h3>
    <div class="entrymeta"></div>
    <div class="entrybody">

    <!-- Comment data returned from database should appear here -->
    <div id="commentsLocation">
    
    <?php
    //connect to database
    require_once("../haveYourSay/db_fns.php");
    require_once("../haveYourSay/data_valid_fns.php");
    $conn = db_connect();
    // display data
    $query = "SELECT * FROM comments WHERE topicId='" . $topic . "' ORDER BY commentId";
    $result = mysql_query($query);
    if(!$result)
    {
        $feedback =  "PROBLEM READING FROM THE DATABASE - SQL SAYS: \n" . mysql_error();
    }

    while($nt=mysql_fetch_array($result))
    {
    ?>
    <div style="margin-top:20px;">
        <hr>
		<!-- create index link to facilitate linking to this particular post -->
        <a name="<?php echo($nt[postDate])?>"></a>
        <ul>
        <li>From: <?php echo($nt[name])?> </li>
        <li>Date: <?php echo($nt[postDate])?></li>
        </ul>
        <?php 
        $temp=nl2br(clean($nt[commentText]));
        echo($temp) ?>
        <br />
    </div>
    <?php
    }
    ?>
    <p align="center"><font color="red">
    <?php echo $feedback ?></font></p>
    
    </div> <!-- END CommentsLocation -->
    
    <!-- Reply from ajax communication -->
    <div id="ajaxReply" style="color:red; display:none"></div>
    <br /><br />
    <div style="margin-top:30px;margin-bottom:20px;">
    <a name="comments"></a> 
        <h3>Add your comment:</h3>
    </div>
    
<script type="text/javascript">

</script>

<form id="cform">
    
    <?php
    echo "<input type='hidden' name='f_id' value='$topic' />";
     ?>
    
        <table style="background-color:#eeeeee; border-width:1px; border-spacing:1px; 
               border-style:outset; border-color:gray; border-collapse:separate; padding:10px" width='100%'>
            <tr>
                <td width='25%'><strong>Name:</strong><br /><small>&nbsp;</small></td>
                <td width='75%'><input type="text" name="f_name" id="f_name" size="25"></td>
            </tr>
            
            <tr>
                <td><strong>Email:</strong><br /><small>(will never be published)</small></td>
                <td><input type="text" name="f_email" id="f_email" size = "40" /></td></td>
            </tr>

            <tr>
                <td><strong>Copy this code:</strong><br /><img src='../images/key.gif' align='bottom' height="20" width="100" /></td>
                <td><input type = 'text' name = 'f_code' ID = 'f_code' size = '20' maxlength = '20' /></td>
            </tr>
            
            <tr>
                <td colspan='2'><small>&nbsp;</small></td>
            </tr>
            
            <tr>
                <td><strong>Your Comment:</strong><br /><small>&nbsp;</small></td>
                <td>&nbsp;</td>
            </tr>
            <tr>
                <td colspan='2'><textarea name="f_comment" id="f_comment" rows="10" cols="65" wrap="physical"></textarea></td>
            </tr>
            <tr>
                <td>&nbsp;<br />&nbsp;</td>
                <td><input type="button" value="Send Comment" onclick="addComment();" /></td>
            </tr>
        </table>
</form>
    
    
<script type="text/javascript">
var lv_name = new LiveValidation('f_name');
lv_name.add(Validate.Presence);
var lv_email = new LiveValidation('f_email');
lv_email.add(Validate.Presence);
lv_email.add(Validate.Email);
var lv_code = new LiveValidation('f_code');
lv_code.add(Validate.Presence);
var lv_comment = new LiveValidation('f_comment');
lv_comment.add(Validate.Presence);


function addComment()
{
    if(LiveValidation.massValidate([lv_name, lv_email, lv_code, lv_comment]))
    {
        new Ajax.Request('../addcomment.php',
        {
            method: 'post',
            parameters: $('cform').serialize(true),
            onLoading:function()
            {
                $('ajaxReply').show();
                $('ajaxReply').innerHTML = "Sending ...";
            },
            onSuccess: function(transport)
            {
                var response = transport.responseText || "No response text";
                $('commentsLocation').innerHTML = response;                
                $('f_code').value='';
                $('ajaxReply').hide();                
            },
            onFailure:function()
            {
                $('ajaxReply').show();
                $('ajaxReply').innerHTML = "Failed to execute...";
            }
        });
    }
    else
    {
        $('ajaxReply').show();
        $('ajaxReply').innerHTML = "All fields need to be verified!";
    }
}
    </script>

<!-- STYLES FOR LIVEVALIDATION -->
<style type="text/css">    
.LV_validation_message{
    font-weight:normal;
    margin:0 0 0 5px;
}

.LV_valid {
    color:#00CC00;
}
    
.LV_invalid {
    color:#CC0000;
}
    
.LV_valid_field,
input.LV_valid_field:hover, 
input.LV_valid_field:active,
textarea.LV_valid_field:hover, 
textarea.LV_valid_field:active {
    border: 1px solid #00CC00;
}
    
.LV_invalid_field, 
input.LV_invalid_field:hover, 
input.LV_invalid_field:active,
textarea.LV_invalid_field:hover, 
textarea.LV_invalid_field:active {
    border: 1px solid #CC0000;
}
</style>
    <p align = "right"><a href="#top">back to top</a></p>
    </div></div>

Note that you can print this code (including the line numbers) by clicking the small print link at the top of each code excerpt. This way it's easier to refer to the respective line numbers discussed below:

Lines 1 to 2
Import the said javascript libraries.
Line 10
The commentsLocation div mentioned earlier on. This is the location where all server returns ends up.
Lines 12 to 36
Echo all the comments currently available for this topic. The $feedback variable will hold the server's response to its actions. Line 36 is responsible for the actual echoing of the comments data.
Line 43
Echo the feedback if a problem occured with the database.
Line 48
This is the ajaxReply div location mentioned earlier.
Lines 59 to 98
The form. Note the hidden field that is set to the value that was set on the page that includes the template code (the $topic variable in the 2-liner).
Note also the button that calls addComment() on line 95.
Lines 101 to 110
These are the LiveValidation methods that handles local validation of the form fields.
Lines 113 to 145
The Ajax functionality handled by the prototype.js library. The server php file is referred to on line 117. Ajax ensures that all the server reply ends up in the comments location area with line 129. The actual validation results from Ajax end up in the AjaxResponse area.
Lines 149 to 176
Styles for the LiveValidation library.


Compare the form in the code above with the previous form we discussed on Using Ajax and PHP, listing 11. Firstly this code uses a table while the other one used fieldset and label tags to organise its form elements. While I like the idea of using fieldset and label tags, unfortunately IE and FF treat them very differently and hence display different results.
But there are major differences, this form does not have an action attribute nor a submit button as the previous form did. Those 2 factors are responsible for the usual synchronous form action. Ajax uses buttons to call javascript functions to send the form asynchronously.
Also, some extra bits have been added, such as an image with a code that the user needs to replicate - this is a very useful addition to avoid spam (I speak from experience here). You need to change this image from time to time and alter the comparison value in the server file addcomment.php as well.

As a sideline, there is the issue of lazy people (like me) giving the 'id' and 'name' attributes the same value. As long as you know that the 'name' value is what the form uses to send to the server, while the id is what the DOM, javascript and CSS uses to identify the fields. Using the same name for both makes it a little easier to code since the browser will handle the implementation correctly anyway. In proper XML, only FORM controls, OBJECTs, PARAMs, and METAs are permitted a name attribute.

back to index

Implementing the Server functionality

The file that resides on the server takes care of saving the data gotten from the form via the POST protocol and reading all the commenting data for a particular page from the database. This data is echoed back to the page of origin, together with a message.

All the server is required to do is echo data, Ajax will take care of how this data will be handled at the client side, once it receives this data in its callback method.

The algorithm of the server code follows. The line numbers correspond with the actual lines in the code.



actions                                                    Lines in code
---------------------------------------------------------------------------
store formdata to variables                                   5 - 9
create local variable for feedback = $feedback                12
connect to the database                                       15                                    
check if the key code is ok                                   18
    if ok 
        update the database with POST information.            20 - 24
        send email to self to notify web administrator.       26 - 29
        if database update failed, 
            set $feedback to DB_ERROR                         35
        else 
            set $feedback to SUCCESS.                         39                 
    else the codekey was bad.
        set $feedback to BAD_KEY.                             44
get all the data from database for this page with id = $id.   50 - 51
if unsuccessful
    set $feedback to DB_ERROR.                                54
else 
    send data in black font.                                  57 - 70
set feedback in red font.                                     75

As you can see, a simple algorithm that should make the code self-explanatory:


<?php
require_once("./haveYourSay/db_fns.php");
require_once("./haveYourSay/data_valid_fns.php");
// save form data into variables
$id=$_POST['f_id'];
$em=$_POST['f_email'];
$na=$_POST['f_name'];
$co=$_POST['f_comment'];
$code = strtolower($_POST['f_code']);

//other variables
$feedback = "";

//connect to database
$conn = db_connect();

// check for code first, if ok, update the database
    if($code == "ADD KEY CODE HERE")
    {
        $query = "INSERT INTO comments VALUES (NULL, now(), '" . $id . "','" . $em . "','" . $na . "','" . $co . "')";
        $result = mysql_query($query);
        $q2= "SELECT url FROM topics WHERE topicId='" . $id . "'";
        $r2 = mysql_query($q2);
        $val = mysql_result($r2, 0, 0);
        //send email to notify self
        mail("YOUR EMAIL URL", 
            "comment From $na, $em", 
            "$message(comment from page $val received from $na)",            
            "From: $em"
            );
        // handle response
        $feedback = "";
        if(!$result)
        {
            $feedback = "PROBLEM WRITING TO THE DATABASE - SQL SAYS: \n" . mysql_error();         
        }
        else
        {
            $feedback = "Your submission was successfull! \n";
        }
    }
    else
    {
        $feedback = "YOU ENTERED THE WRONG CODE! \n Make sure you don't use spaces and try again. \n";
    }

// display data. This is not conditionaly because even if the form entry 
// was  incorrect we still need to display the data in order to 
// asynchronously update the caller page
$query = "SELECT * FROM comments WHERE topicId='" . $id . "' ORDER BY commentId";
$result = mysql_query($query);
if(!$result)
{
    $feedback =  "PROBLEM READING FROM THE DATABASE - SQL SAYS: \n" . mysql_error();
}

while($nt=mysql_fetch_array($result))
{
?>
<div style="margin-top:20px;">
    <hr>
	<!-- create index link to facilitate linking to this particular post -->
    <a name="<?php echo($nt[postDate])?>"></a>
    <ul>
    <li>From: <?php echo($nt[name])?></li>
    <li>Date: <?php echo($nt[postDate])?></li>
    </ul>
    <?php 
    $temp=nl2br(clean($nt[commentText]));
    echo($temp) ?>
    <br />
</div>
<?php
}
?>
<p align="center"><font color="red">
<?php echo (nl2br($feedback)) ?></font></p>
	

It would be easy to expand this email facility so users can tick a box if they want to be notified of further correspondence on the forum. In that case the email URL will need to be checked more stringently and more security measures taken to avoid bad people using your forum for posting email spam.

back to index

Trying out the commenting system

You can try out this commenting system below - as said earlier, play around with entering wrong values first and try to send the form. After that, send me a valid comment using the occasion to give me some feedback on this article at the same time. I will delete all comments that are obviously just test runs.

As you will see, the form validation appears immediately as soon as you leave the field. The validation results are displayed next to the form fields in red.
All the data returned by the server is displayed in the commnentsLocation div area. This consists of the actual comments data followed by red text to indicate the feedback from the server actions. For that reason, you may notice that the code key validation happens after you submitted the form data to the server.
Lastly, Ajax also gives feedback in the ajaxReply div area. These replies provide further feedback to the user regarding the transport communication and general state of validation.

back to index


Comments