Codular

HomeWriters RSS

Part 1: Build a Rating System

Introduction

On a lot of sites that you visit there is the option to rate something, most working off a 5 star system. Here we're going to take a really simple CSS/HTML solution for rating stars that Harry Roberts made and turn it into a dynamic jQuery and PHP rating system. In this first half, we'll be looking at how to create a nice slick jQuery interactive experience for your users, then in the second part we'll look at the backend section, how to receive, store and return the response back to the front end.

The HTML/CSS

We're going to take this awesome code and adapt it to our needs. Firstly, we only need one set of rating stars, so we'll start with just one div. We have to store the default (or current) rating, for that we'll use a simple data-attribute of data-default, you can also add an s-# class where # is the same as the data-default value.

Remember, the number of stars must be an increment of 0.5. Here is a sample div for something with a current rating of 0.5:

<div class='stars s-0.5' data-default='0.5'>
    0.5 Stars
</div>

We want to keep the text within the div for accessibility. Now that we have that written, we will be using the same CSS that Harry had his the earlier JSFiddle, there is no need for us to change that.

The jQuery

This is the part where we take the HTML/CSS that we have above and make it dynamic. There are three initial things we are trying to achieve:

  1. Give the user feedback to show what rating they are 'selecting' as they move the mouse over the stars - in increments of 0.5.
  2. When the user takes their mouse off the rating stars, it reverts back to the original/default rating that we stored in data-default.
  3. If the user clicks, we want to send an AJAX request off to the rating PHP script sending the rating that the user has chosen

$(body).on

We could quite simply go through and write out three separate chunks of code to bind events to the div.stars elements that we have, however to make things awesome, neat and super-fantastic we'll use the jQuery method .on(). This has the benefit of being able to delegate an event, which means that will fire on dynamically injected/inserted elements that match the selector too.

So to add a delegated event, we choose a parent of the element that we want - in this case we'll use the ever-present element body. We can also pass an object of events that we want fired. Our basic event handling will be done with the following base:

$('body').on({
    mousemove: function(){

    },
    mouseleave:function(){

    },
    click: function(){

    }
}, '.stars');

Event: mousemove

We use this event instead of the hover event as we want to get the mouse position whenever it is moved around within the element, instead of only once when the cursor enters the element. Firstly we want to calculate what class we 0.5 increment of stars we want, to do that we need to take the current mouse position, element width and then do some rounding, multiplication and division to get this into a 0.5 increment:

var currentMousePosition = e.pageX - $(this).offset().left;

This will return the current mouse position relative to the left hand side of the element. It takes the x-position of the cursor on the whole page, and then subtracts the x-position of the left hand side of the element.

You'll notice that we're using e.pageX, here e refers to the event, so this will be passed through the jQuery function as the first attribute so you'll need to put an e within that function() straight after the mousemove event.

To get the element width we can simply use $(this).width(), nothing too complex there. Next, we'll take the cursor position divided by the element width and multiply it by 10, and then round it. This will give us the position to the nearest whole number - meaning that we can simply divide by 2 to get the final increment of 0.5 that we want:

var currentMousePosition = e.pageX - $(this).offset().left;
var width = $(this).width();
var rounded = Math.round((currentMousePosition/width)*10);

var starNumber = rounded/2;

This is now the number of stars that we will show the user, it steps up in nice increments of 0.5, now we need to look at adding the required class to the stars div, and store the current selected value in another data-attribute.

First we want to remove all classes on the element, then simply add back the stars class and our s-# star, where # is the starNumber value from before:

$(this).removeClass();
$(this).addClass('stars s-' + starNumber);

With that, we'll now store the current star count into the data-attribute called data-rating - which we can then use later to get the rating when the user clicks on their chosen rating:

$(this).attr('data-rating', starNumber);

We can then throw all of this together and we have our mousemove event:

// Calculate number of stars
var currentMousePosition = e.pageX - $(this).offset().left;
var width = $(this).width();
var rounded = Math.round((currentMousePosition/width)*10);

var starNumber = rounded/2;

// Remove + add Classes
$(this).removeClass();
$(this).addClass('stars s-' + starNumber);

// Store current rating
$(this).attr('data-rating', starNumber);

Event: mouseleave

Simply revert the element back to displaying the number of stars that it had before everything - this would be the value that we stored on load in the data-default attribute:

$(this).removeClass();
$(this).addClass('stars s-' + $(this).attr('data-default'));

Simple as that, nothing more to it!

Event: click

This is where the fun starts to come in, we have a lot to do with the click event, you can leave some of it out if you don't care about your users, but if you do, you're a bad person! Here we need to show a loading icon, send the request, wait for the response, set the number of showed stars (to the returned value) and reset the default value.

  1. Show a loading icon

For this, we'll use the super awesome AjaxLoad.info site to give us an image to use, I'm going to go for the 2nd to last image as I think it's the simplest and smallest to fit in where we want it. I've quickly thrown some CSS together for that image:

.loading {
    background: url('data:image/gif;base64,R0lGODlhEAALAPQAAP///wAAANra2tDQ0Orq6gYGBgAAAC4uLoKCgmBgYLq6uiIiIkpKSoqKimRkZL6+viYmJgQEBE5OTubm5tjY2PT09Dg4ONzc3PLy8ra2tqCgoMrKyu7u7gAAAAAAAAAAACH+GkNyZWF0ZWQgd2l0aCBhamF4bG9hZC5pbmZvACH5BAALAAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAEAALAAAFLSAgjmRpnqSgCuLKAq5AEIM4zDVw03ve27ifDgfkEYe04kDIDC5zrtYKRa2WQgAh+QQACwABACwAAAAAEAALAAAFJGBhGAVgnqhpHIeRvsDawqns0qeN5+y967tYLyicBYE7EYkYAgAh+QQACwACACwAAAAAEAALAAAFNiAgjothLOOIJAkiGgxjpGKiKMkbz7SN6zIawJcDwIK9W/HISxGBzdHTuBNOmcJVCyoUlk7CEAAh+QQACwADACwAAAAAEAALAAAFNSAgjqQIRRFUAo3jNGIkSdHqPI8Tz3V55zuaDacDyIQ+YrBH+hWPzJFzOQQaeavWi7oqnVIhACH5BAALAAQALAAAAAAQAAsAAAUyICCOZGme1rJY5kRRk7hI0mJSVUXJtF3iOl7tltsBZsNfUegjAY3I5sgFY55KqdX1GgIAIfkEAAsABQAsAAAAABAACwAABTcgII5kaZ4kcV2EqLJipmnZhWGXaOOitm2aXQ4g7P2Ct2ER4AMul00kj5g0Al8tADY2y6C+4FIIACH5BAALAAYALAAAAAAQAAsAAAUvICCOZGme5ERRk6iy7qpyHCVStA3gNa/7txxwlwv2isSacYUc+l4tADQGQ1mvpBAAIfkEAAsABwAsAAAAABAACwAABS8gII5kaZ7kRFGTqLLuqnIcJVK0DeA1r/u3HHCXC/aKxJpxhRz6Xi0ANAZDWa+kEAA7AAAAAAAAAAAA');
    width: 16px;
    height: 11px;
}

Now that we have an element that we can use to show the loading symbol - <div class='loading'></div> - we can now use the jQuery method replaceWith() to replace the clicked on element with that loading image:

$(this).replaceWith($('<div>', {
    'class': 'loading'
}));
  1. Send the request

We'll be using the $.post() method that will send a POST request to a script that we want, we pass it a URL, some parameters (namely the rating) and get a response. We get the rating by accessing the previously stored data-rating attribute, and we will be expecting a response back in a JSON format, so we have to define that too. Our basic code will look something like what is below, you'll notice the result function is missing its contents, we'll get to that next:

$.post('rating.php',{
    rating: $(this).attr('data-rating')
}, function(d){

}, 'json');

The result function should:

Our anticipated returned JSON will come in two formats, the first below being failure and the second being success:

{ 
    result: 'error',
    msg: 'Unable to add rating - unknown error.'
}

{
    result: 'success',
    rating: 4.5
}

Therefore to handle these responses we'd use something galong the lines of:

if(d.result == 'error'){
    alert(d.msg);
} else {
    $(this).removeClass();
    $(this).addClass('stars s-' + d.rating);
    $(this).attr('data-default', d.rating);
    var $temp = $(this);
    $('.loading').replaceWith($temp);
}

Throw it all together and we'd get a click event along the lines of:

//Hide the current rating selector
$(this).replaceWith($('<div>', {
    'class': 'loading'
}));

// Send the request
$.post('rating.php',{
    rating: $(this).attr('data-rating')
}, function(d){
    // Handle response
    if(d.result == 'error'){
        alert(d.msg);
    } else {
        $(this).removeClass();
        $(this).addClass('stars s-' + d.rating);
        $(this).attr('data-default', d.rating);
        $temp = $(this);
        $('.loading').replaceWith($temp);
    }
}, 'json');

Throw the jQuery together

Now that we have all of the sections, we can throw them all together and look to see things working - unfortunately as we haven't yet written the backend code to this, the rating system won't do a huge amount at the moment, but it should at least show the loading segment for you, and also make use of the mousemove and mouseleave events.

$('body').on({
    mousemove: function(e){
        // Calculate number of stars
        var currentMousePosition = e.pageX - $(this).offset().left;
        var width = $(this).width();
        var rounded = Math.round((currentMousePosition/width)*10);

        var starNumber = rounded/2;

        // Remove + add Classes
        $(this).removeClass();
        $(this).addClass('stars s-' + starNumber);

        // Store current rating
        $(this).attr('data-rating', starNumber);
    },
    mouseleave:function(){
        $(this).removeClass();
        $(this).addClass('stars s-' + $(this).attr('data-default'));
    },
    click: function(){
        //Hide the current rating selector
        $(this).replaceWith($('<div>', {
            'class': 'loading'
        }));

        // Send the request
        $.ajax('rating.php',{
            rating: $(this).attr('data-rating')
        }, function(d){
            // Handle response
            if(d.result == 'error'){
                alert(d.msg);
            } else {
                $(this).removeClass();
                $(this).addClass('stars s-' + d.rating);
                $(this).attr('data-default', d.rating);
                $temp = $(this);
                $('.loading').replaceWith($temp);
            }
        }, 'json');
    }
}, '.stars');

Optimisation for the nation

If you went with that code, everything would work fine, however it wouldn't be overly efficient or optimised. One of the main things to look at is element caching - which means assigning things like $(this) withing each event to a variable. We can also chain a lot of methods that relate to the same element, saving lines and characters - yey!

If we take the mousemove event from abouve, a more optimised version would sum up to be:

// Cache $(this)
var $this = $(this);

// Calculate number of stars
var currentMousePosition = e.pageX - $this.offset().left;
var width = $this.width();
var rounded = Math.round((currentMousePosition/width)*10);

var starNumber = rounded/2;

// Remove + add Classes + Chain both
// Store current rating + Chain!
$this.removeClass().addClass('stars s-' + starNumber).attr('data-rating', starNumber);

Simple as that, making our code neater, faster, better and sexy.

Roundup

There are some extra ways that you can improve this system, look to implement a disabled option so that you can stop people multiple rating, also add the ability to change the text within the div as the user changes their rating choices.

Ultimately that is the completed front-end chunk of code, you can see my completed code over on my JSFiddle, have a play and look out for part 2 that will guide you through the PHP code that will receive and store the rating in a database for you. In that tutorial we'll be using a large amount of OO code, so be sure to go and read through our Introduction to PHP Classes.

Tag: jQuery