Blog (my web musings)

Find out what's interesting me, get tips, advice, and code gems that don't fit elsewhere.


Search Blog


I was browsing the CSS Tricks forums the other day and came across a thread about this Star Rating CSS demo by Geoffrey Crofte. Nice! It got me thinking again about my AJAX & PHP Click-Counter with Flat File Storage (text file) script from a few years back, and whether I could adapt it to store the star ratings in instead. If you haven't read that article already, you might want to head over there to give it a quick once-over because I'll loosley be refering back to various parts of it below.

In case you can't wait until the end of the article, here's a sneak peek of the final star rating script - again done without jQuery or the likes, so it stays nice and light-weight. AJAX & PHP 5-Star Rating with Flat File Storage (text file):

Demo

HTML markup

The HTML was the first and easiest thing to set up. I applied the required click-trigger class and data-click-id attrubute to a series of anchors that would be the stars;

<div class="rating">
    <div class="rating-stars">
        <a href="#5" class="click-trigger star-5" data-click-id="star-5" title="Vote 5 stars">★</a>
        <a href="#4" class="click-trigger star-4" data-click-id="star-4" title="Vote 4 stars">★</a>
        <a href="#3" class="click-trigger star-3" data-click-id="star-3" title="Vote 3 stars">★</a>
        <a href="#2" class="click-trigger star-2" data-click-id="star-2" title="Vote 2 stars">★</a>
        <a href="#1" class="click-trigger star-1" data-click-id="star-1" title="Vote 1 star">★</a>
    </div>
</div>

Just like the earlier click-counter script, the click-trigger class is used by the JavaScript to identify the clickable elements, and the data-click-id attrubutes are used in the text storage file alongside recorded votes / ratings.

JavaScript / AJAX

Taking the previous click-counter JavaScript, I tidied up the variables by defining them all at the top, and added in a mechanism to check for previous votes using localStorage;

var clicks = document.querySelectorAll('.click-trigger'),
    voted = localStorage.getItem('rating'),
    message = document.getElementById('rating-message'),

    length = clicks.length, i, id, post, req, countArray;
for(i = 0; i < length; i++){
    clicks[i].onclick = function(){
        if (voted == 'voted') {
            message.innerHTML = "You've already voted!";
            message.className = 'voted';
            } else {

            id = this.getAttribute('data-click-id');
            post = 'id='+id; // post string
            req = new XMLHttpRequest();
            req.open('POST', 'counter.php', true);
            req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            req.onreadystatechange = function(){
                if (req.readyState != 4 || req.status != 200) return;
                //document.getElementById(id).innerHTML = req.responseText;
                };
            req.send(post);
            localStorage.setItem('rating', 'voted');
            voted = 'voted';
            }

        }
    }

This is easier to understand reading from the bottom up, where, after a vote has taken place, the 'rating' item is set in localStorage with a value of 'voted', as is a local 'voted' variable.

Leaping to the top of the script, you can see where the 'rating' item is retrived from localStorage and assigned to the 'voted' variable. If a vote has already been submitted, a value of 'voted' will exist in localStorage, but if a vote has not taken place, the value will be 'null'.

In the middle, we check if the 'voted' variable has a value of 'voted' and, if it does, we return a "You've already voted!" message, modifying the innerHTML of an empty #rating-message element;

<div id="rating-message"></div>

I removed the old responseText (which would have returned a single click-count for individual elements from the "counter.php" file) because I wanted to eventually return a rating average and total number of votes. This would require two values coming back from the server instead of just one. After a bit of thought, I decided that the easiest way to seperate multiple values was with a || delimiter, like those used in the "counter.txt" file. My rating average and total number of votes would therefore come back from the server in this kind of string format;

3.75||36

These values can conveniently be seperated with JavaScript using .split(), which divides a string (at the specified || delimiter) into an array of substrings. I inserted three new lines in place of the old responseText;

                if (req.readyState != 4 || req.status != 200) return; 
                //document.getElementById(id).innerHTML = req.responseText;
                countArray = req.responseText.split('||');
                document.getElementById('count-avg').innerHTML = countArray[0];
                document.getElementById('count-total').innerHTML = countArray[1];
                };

The array items / substrings can now be accessed using countArray[0] and countArray[1] and used to modify the innerHTML of a #count-avg and #count-total element on the page.

Rated <span id="count-avg"></span>/5 (<span id="count-total"></span> Votes)

With this plan, I knew what I was going to do with the rating average and total number of votes once I had them, but as I hadn't yet calculated them, I went away to focus on that.

So, thinking about how all the ratings were to be stored in the "counter.txt" file;

star-1||1
star-2||2
star-3||8
star-4||19
star-5||6

I concluded that it would need to be tackled in two stages;

    1. Calculations visible on the page as soon as it loads
    2. Calculations returned in real time when the rating stars are selected

PHP

Reusing the server-side processing script from the click-counter ("counter.php"), I removed the no longer needed $num echo (the rest of the script was fine and continues to be used), and concentrated on the calculations for rating average and total votes;

<?php

$file = 'counter.txt'; // path to text file that stores counts
$fh = fopen($file, 'r+');
$id = $_REQUEST['id']; // posted from page
$lines = '';
while(!feof($fh)){
    $line = explode('||', fgets($fh));
    $item = trim($line[0]);
    $num = trim($line[1]);
    if(!empty($item)){
        if($item == $id){
            $num++; // increment count by 1
            //echo $num;
            }
        $lines .= "$item||$num\r\n";
        }
    }
file_put_contents($file, $lines);
fclose($fh);


/* #### - rating average and total votes - #### */

$all_lines = file($file, FILE_IGNORE_NEW_LINES);

foreach($all_lines as $line){
    list($item, $num) = explode('||', $line);
    $count[$item] = $num;
    }

$avg = ($count['star-1']*1 + $count['star-2']*2 + $count['star-3']*3 + $count['star-4']*4 + $count['star-5']*5) / array_sum($count);

echo number_format($avg,2,'.','').'||'.array_sum($count); // sent back to page

?>

In the additional code, file() reads all the lines from the "counter.txt" file into an $all_lines array. From there, we loop through each line, exploding at '||' to assign each item ID as a key that can be used to more easily reference the associated number count.

With the average calculated, it can be formatted to 2 decimal places and echo'd back to the page, along with the total number of votes. If you recall from earlier, these are the || delimited values that the JavaScript picks up and splits, before modifying the innerHTML of the below elements;

Rated <span id="count-avg">3.75</span>/5 (<span id="count-total">36</span> Votes)

The last thing to do is to get the calculations onto the page when it first loads. Again, we can do this with PHP, and luckily, we can reuse the extra code added to the "counter.php" file, albeit with a few checks so that zero ratings don't generate unsightly PHP errors. This code goes into the web page;

<?php 

$file = 'counter.txt';
$all_lines = file($file, FILE_IGNORE_NEW_LINES);

foreach($all_lines as $line){
    list($item, $num) = explode('||', $line);
    $count[$item] = $num;
    }
if(array_sum($count) != 0){
    $avg = number_format(($count['star-1']*1 + $count['star-2']*2 + $count['star-3']*3 + $count['star-4']*4 + $count['star-5']*5) / array_sum($count),2,'.','');
    } else {
    $avg = 0;
    }
?>

Before we echo the figures into the HTML;

Rated <span id="count-avg"><?php echo $avg;?></span>/5 (<span id="count-total"><?php echo array_sum($count);?></span> Votes)

Don't worry of you didn't catch all that. There's a full working example script, including CSS, HTML, JavaScript / AJAX, and PHP, in the download pack on the demo page;

Demo