Simple AJAX Commenting System
This time, we are making a Simple AJAX Commenting System. It will feature a gravatar integration and demonstrate how to achieve effective communication between jQuery and PHP/MySQL with the help of JSON.
Step 1 – XHTML
First, lets take a look at the markup of the comments. This code is generated by PHP in the Comment class, which we are going to take a look at in a moment.
demo.php
<div class="comment"> <div class="avatar"> <a href="http://tutorialzine.com/"> <img src="http://www.gravatar.com/avatar/112fdf7a8fe3609e7af2cd3873b5c6bd?size=50&default=http%3A%2F%2Fdemo.tutorialzine.com%2F2010%2F06%2Fsimple-ajax-commenting-system%2Fimg%2Fdefault_avatar.gif"> </a> </div> <div class="name"><a href="http://tutorialzine.com/">Person's Name</a></div> <div title="Added at 06:40 on 30 Jun 2010" class="date">30 Jun 2010</div> <p>Comment Body</p> </div>
The avatar div contains a hyperlink (if the user entered a valid URL when submitting the comment) and an avatar image, which is fetched from gravatar.com. We will return to this in the PHP step of the tut. Lastly we have the name and time divs, and the comment body.
The other important element in the XHTML part is the comment form. It is sent via POST. All fields except for the URL field are required.
demo.php
<div id="addCommentContainer"> <p>Add a Comment</p> <form id="addCommentForm" method="post" action=""> <div> <label for="name">Your Name</label> <input type="text" name="name" id="name" /> <label for="email">Your Email</label> <input type="text" name="email" id="email" /> <label for="url">Website (not required)</label> <input type="text" name="url" id="url" /> <label for="body">Comment Body</label> <textarea name="body" id="body" cols="20" rows="5"></textarea> <input type="submit" id="submit" value="Submit" /> </div> </form> </div>
The form is submitted via AJAX. The validation is performed entirely in the backend by submit.php, as you will see in the jQuery step of the tutorial. Every field has a corresponding label element, with an appropriate for attribute.
Step 2 – PHP
PHP handles the communication with the MySQL database and generates the markup of the comments. It is also on the receiving end of the AJAX requests and inserts the comment data to the comments table. You can see the code that prints the comments to the page below.
demo.php
/* / Select all the comments and populate the $comments array with objects */ $comments = array(); $result = mysql_query("SELECT * FROM comments ORDER BY id ASC"); while($row = mysql_fetch_assoc($result)) { $comments[] = new Comment($row); }
The MySQL query selects all the entries from the database and fills the $comments array with objects of the comment class, which you will see below. This array is outputted later in the execution of the script.
demo.php
/* / Output the comments one by one: */ foreach($comments as $c){ echo $c->markup(); }
Each comment has a markup() method, which generates valid HTML code ready to be printed to the page. You can see the definition of this method and the class below.
The class takes a row from the database (fetched with mysql_fetch_assoc() ) and stores it in the private variable $data. It is available only to the methods of the class and cannot be accessed from outside.
comment.class.php – Part 1
class Comment { private $data = array(); public function __construct($row) { /* / The constructor */ $this->data = $row; } public function markup() { /* / This method outputs the XHTML markup of the comment */ // Setting up an alias, so we don't have to write $this->data every time: $d = &$this->data; $link_open = ''; $link_close = ''; if($d['url']){ // If the person has entered a URL when adding a comment, // define opening and closing hyperlink tags $link_open = '<a href="'.$d['url'].'">'; $link_close = '</a>'; } // Converting the time to a UNIX timestamp: $d['dt'] = strtotime($d['dt']); // Needed for the default gravatar image: $url = 'http://'.dirname($_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"]). '/img/default_avatar.gif'; return ' <div class="comment"> <div class="avatar"> '.$link_open.' <img src="http://www.gravatar.com/avatar/'. md5($d['email']).'?size=50&default='. urlencode($url).'" /> '.$link_close.' </div> <div class="name">'.$link_open.$d['name'].$link_close.'</div> <div class="date" title="Added at '. date('H:i \o\n d M Y',$d['dt']).'">'. date('d M Y',$d['dt']).'</div> <p>'.$d['body'].'</p> </div> '; }
This script uses gravatar to present avatars in the comments. For those of you, who have not used gravatar, this is a really useful service, which lets you associate an avatar with your email address. The avatar image can easily be fetched by passing an md5() encoded hash of your email address to gravatar.com. This is exactly what we do on line 48.
Notice line 39 above it – the script tries to figure out the URL at which it is located, and determines the exact address of the default_avatar.gif image. This gif is passed to gravatar along the md5 hash, so if no avatar was found for this particular email, the fallback image is displayed instead.
comment.class.php – Part 2
public static function validate(&$arr) { /* / This method is used to validate the data sent via AJAX. / / It return true/false depending on whether the data is valid, and populates / the $arr array passed as a paremter (notice the ampersand above) with / either the valid input data, or the error messages. */ $errors = array(); $data = array(); // Using the filter_input function introduced in PHP 5.2.0 if(!($data['email'] = filter_input(INPUT_POST,'email',FILTER_VALIDATE_EMAIL))) { $errors['email'] = 'Please enter a valid Email.'; } if(!($data['url'] = filter_input(INPUT_POST,'url',FILTER_VALIDATE_URL))) { // If the URL field was not populated with a valid URL, // act as if no URL was entered at all: $url = ''; } // Using the filter with a custom callback function: if(!($data['body'] = filter_input(INPUT_POST,'body',FILTER_CALLBACK, array('options'=>'Comment::validate_text')))) { $errors['body'] = 'Please enter a comment body.'; } if(!($data['name'] = filter_input(INPUT_POST,'name',FILTER_CALLBACK, array('options'=>'Comment::validate_text')))) { $errors['name'] = 'Please enter a name.'; } if(!empty($errors)){ // If there are errors, copy the $errors array to $arr: $arr = $errors; return false; } // If the data is valid, sanitize all the data and copy it to $arr: foreach($data as $k=>$v){ $arr[$k] = mysql_real_escape_string($v); } // Ensure that the email is in lower case (for a correct gravatar hash): $arr['email'] = strtolower(trim($arr['email'])); return true; }
The validate() method above (also part of the class) is defined as static. This means that it can be evoked directly like Comment::validate(), without the need of creating an object of the class. What this method does, is validate the input data which is submitted via AJAX.
This method uses the new filter functions, which are available as of PHP 5.2.0. These allow us to easily validate and filter any input data that is passed to the script. For example filter_input(INPUT_POST,’url’,FILTER_VALIDATE_URL) means that we are checking whether $_POST['url'] is a valid URL address. If it is, the function returns the value of the variable, otherwise it return false.
This is really useful, as up until now, we had to use custom regular expressions to validate data (and have series of if statements). Also, another advantage is that this data is fetched before any configuration-specific transformations (like magic quotes) are applied.
We also have the option of specifying a custom function which is going to apply some more advanced modifications of the data, as you can see from lines 31 and 37.
comment.class.php – Part 3
private static function validate_text($str) { /* / This method is used internally as a FILTER_CALLBACK */ if(mb_strlen($str,'utf8')<1) return false; // Encode all html special characters (<, >, ", & .. etc) and convert // the new line characters to <br> tags: $str = nl2br(htmlspecialchars($str)); // Remove the new line characters that are left $str = str_replace(array(chr(10),chr(13)),'',$str); return $str; } }
The last method is validate_text, which we are passing as a callback function in the two filter_input calls above. It encodes all special HTML characters, effectively blocking XSS attacks. It also replaces the newline characters with <br /> line breaks.
submit.php
/* / This array is going to be populated with either / the data that was sent to the script, or the / error messages: /*/ $arr = array(); $validates = Comment::validate($arr); if($validates) { /* Everything is OK, insert to database: */ mysql_query(" INSERT INTO comments(name,url,email,body) VALUES ( '".$arr['name']."', '".$arr['url']."', '".$arr['email']."', '".$arr['body']."' )"); $arr['dt'] = date('r',time()); $arr['id'] = mysql_insert_id(); /* / The data in $arr is escaped for the mysql insert query, / but we need the unescaped text, so we apply, / stripslashes to all the elements in the array: /*/ $arr = array_map('stripslashes',$arr); $insertedComment = new Comment($arr); /* Outputting the markup of the just-inserted comment: */ echo json_encode(array('status'=>1,'html'=>$insertedComment->markup())); } else { /* Outputting the error messages */ echo '{"status":0,"errors":'.json_encode($arr).'}'; }
submit.php receives the comment form data via an AJAX request. It validates it and outputs a JSON object with either the XHTML markup of the successfully inserted comment, or a list of error messages. jQuery uses the status property to determine whether to display the error messages or add the comment markup to the page.
You can see two example responses below.
Successful response
{ "status": 1, "html": "Html Code Of The Comment Comes Here..." }
The html property contains the code of the comment, similar to markup in step one.
Failure response
{ "status": 0, "errors": { "email": "Please enter a valid Email.", "body": "Please enter a comment body.", "name": "Please enter a name." } }
On failure, jQuery loops through the errors object, and outputs the errors next to the fields that caused them.
Step 3 – CSS
Now that we have all the markup properly generated and displayed on the page, we can move on to styling it.
styles.css – Part 1
.comment, #addCommentContainer{ /* Syling the comments and the comment form container */ padding:12px; width:400px; position:relative; background-color:#fcfcfc; border:1px solid white; color:#888; margin-bottom:25px; /* CSS3 rounded corners and drop shadows */ -moz-border-radius:10px; -webkit-border-radius:10px; border-radius:10px; -moz-box-shadow:2px 2px 0 #c2c2c2; -webkit-box-shadow:2px 2px 0 #c2c2c2; box-shadow:2px 2px 0 #c2c2c2; } .comment .avatar{ /* / The avatar is positioned absolutely, / and offset outside the comment div /*/ height:50px; left:-70px; position:absolute; width:50px; background:url('img/default_avatar.gif') no-repeat #fcfcfc; /* Centering it vertically: */ margin-top:-25px; top:50%; -moz-box-shadow:1px 1px 0 #c2c2c2; -webkit-box-shadow:1px 1px 0 #c2c2c2; box-shadow:1px 1px 0 #c2c2c2; }
The .comment divs and the #addCommentContainer are styled at once because they share most of the styling. A number of CSS3 rules are applied, including rounded corners and a box-shadow. Needless to say, these do not work in older browsers, but as they are purely presentational, the script will still work without them.
styles.css – Part 2
.comment .avatar img{ display:block; } .comment .name{ font-size:20px; padding-bottom:10px; color:#ccc; } .comment .date{ font-size:10px; padding:6px 0; position:absolute; right:15px; top:10px; color:#bbb; } .comment p, #addCommentContainer p{ font-size:18px; line-height:1.5; } #addCommentContainer input[type=text], #addCommentContainer textarea{ /* Styling the inputs */ display:block; border:1px solid #ccc; margin:5px 0 5px; padding:3px; font-size:12px; color:#555; font-family:Arial, Helvetica, sans-serif; } #addCommentContainer textarea{ width:300px; } label{ font-size:10px; } label span.error{ color:red; position:relative; right:-10px; } #submit{ /* The submit button */ background-color:#58B9EB; border:1px solid #40A2D4; color:#FFFFFF; cursor:pointer; font-family:'Myriad Pro',Arial,Helvetica,sans-serif; font-size:14px; font-weight:bold; padding:4px; margin-top:5px; -moz-border-radius:4px; -webkit-border-radius:4px; border-radius:4px; } #submit:hover{ background-color:#80cdf5; border-color:#52b1e2; }
In the second part of the stylesheet, we style the comment and form elements. Notice the input[type=text] selector, which selects elements depending on the type attribute.
Step 4 – jQuery
Now lets continue with jQuery, which is the last step of this tutorial. After including the library to the bottom of the page (best for the perceived performance of the page) we can start coding the script file.
script.js
$(document).ready(function(){ /* The following code is executed once the DOM is loaded */ /* This flag will prevent multiple comment submits: */ var working = false; /* Listening for the submit event of the form: */ $('#addCommentForm').submit(function(e){ e.preventDefault(); if(working) return false; working = true; $('#submit').val('Working..'); $('span.error').remove(); /* Sending the form fileds to submit.php: */ $.post('submit.php',$(this).serialize(),function(msg){ working = false; $('#submit').val('Submit'); if(msg.status){ /* / If the insert was successful, add the comment / below the last one on the page with a slideDown effect /*/ $(msg.html).hide().insertBefore('#addCommentContainer').slideDown(); $('#body').val(''); } else { /* / If there were errors, loop through the / msg.errors object and display them on the page /*/ $.each(msg.errors,function(k,v){ $('label[for='+k+']').append('<span class="error">'+ v+'</span>'); }); } },'json'); }); });
Starting from the top, we have the $(document).ready() call, which binds a function to the DOM content loaded event. The working variable acts as a flag, which tells the script if an AJAX request is in progress (thus preventing double posting).
In the callback function for the POST AJAX request, we check the status property to determine whether the comment was successfully inserted. If it was, we add the received markup to the page after the last comment with a slideDown animation.
If there were problems, we display the error messages by appending an error span to the appropriate label element (the for attribute of the label contains the id of the input that caused the error).
With this our Simple AJAX Commenting System is complete!
Conclusion
To be able to run this script on your server, you need to create the comments table in your MySQL database. You can do this by executing the SQL code found in table.sql from the SQL tab of phpMyAdmin. After this you need to enter your MySQL connection details in connect.php.
You are free to modify and use this code any way you see fit.
What do you think? How would you improve this script?