jQuery php voting widget

Tuesday, 1 December 2009


This tutorial looks at how to build a jQuery UI themeable voting widget using a jQuery sortable and a bit of php to store the current number of votes. The actual widget itself is quite light-weight and the only markup we need is a single unordered list. The jQuery is also minimal and most of the work is done in php to work out the what the current tally is and to store this information.



You can view the working demo this tutorial here.

You can download the code for this tutorial here.

In my implementation I have chosen to store the current number of votes as a single serialized string in a flate file, but this could easily be done in a database.

To begin with I will show you the unordered list, my list is going to contain Charlton Heston movies:
<li class="non-sortable" style="height:2.2em;">Rank your favourite Charlton Heston movies by dragging options</li>
<li class="ui-state-default movie-rank" id="movie-1"><span class='counter'>#1</span>The Ten Commandments</li>
<li class="ui-state-default movie-rank" id="movie-2"><span class='counter'>#2</span>Ben-Hur</li>
<li class="ui-state-default movie-rank" id="movie-3"><span class='counter'>#3</span>Planet of the Apes</li>
<li class="ui-state-default movie-rank" id="movie-4"><span class='counter'>#4</span>The Omega Man</li>
<li class="ui-state-default movie-rank" id="movie-5"><span class='counter'>#5</span>Soylent Green</li>
<li class="ui-state-default movie-rank" id="movie-6"><span class='counter'>#6</span>The Three Musketeers</li>
<li class="ui-state-default movie-rank" id="movie-7"><span class='counter'>#7</span>Antony and Cleopatra</li>
<li class="ui-state-default movie-rank" id="movie-8"><span class='counter'>#8</span>Beneath the Planet of the Apes </li>
<li class="ui-state-default movie-rank" id="movie-9"><span class='counter'>#9</span>El Cid</li>
<li class="ui-state-default movie-rank" id="movie-10"><span class='counter'>#10</span>Julius Caeser</li>
<li class="non-sortable"><button class="ui-button ui-state-default ui-corner-all" type="button" name="submit" style="float:right;">Submit</button></li>

If you look at the li classes above you will see there are two main types, the non-sortable and movie-rank. The jQuery sortable plugin allows us to specify which items should be sortable using the 'items' option and which items will not be part of the sorting process (that is, they stay put). We want our widget title and the submit button to stay put so lets look at the jQuery initialisation:
$("document").ready(function() {
 $("#sortable").sortable({
  placeholder: 'ui-state-highlight',
  items: '.movie-rank',
  cancel: '.non-sortable'
 });
 
 $("#sortable").disableSelection();

The three options from our sortable initialisation are placeholder, items and cancel. Place holder simply provides an indicator for where the item will be dropped, in my example this is the blue bar that appears as the item being dragged hovers over each list item. The items and cancel work as previously mentioned, basically 'items' are sortable and 'cancel' stays put. The final call to disableSelection is an undocumented jQuery UI method that prevents text selection happening on a list element so that grabbing an item to be dragged is easier and the text selection doesn't get the focus first.

Next I decided to add an update function whenever the sort order is changed. This updates the ranking of each item, so if you move #3 to the #7 position all of the numbers update to show your preferred voting order. Let's look at the code:
$('#sortable').bind('sortupdate', function(event, ui) {
 $(".counter").each(function() {
  $(this).empty();
  var index = $(".counter").index(this) + 1;
  $(this).text("#" + index);
 });
});

The sortupdate function is a special function specific to the jQuery UI sortable plugin. You can see all of the options and functions here. This is called when the sort order is updated. I simply remove all of the rankings and then update each of the elements according to their actual position amongst thier peers. This is done using the jQuery index function, which takes in a group of elements and finds the index of the element amongst that group (0 based).

The last piece of jQuery code is our submit function:
$("#sortable [name='submit']").click(function() {
 var rankings = $('#sortable').sortable('serialize');
 
 $.get("vote.php?" + rankings, {}, function(data) {
  $("#sortable").empty().html(data);
 });
});

Our sortable has another great funcion, serialize which looks at the IDs of our li elements and passes them in order as follows: arrayname-id. So I have mine set up as movie-1, movie-2 etc. so these will be returned as movie[]=1,movie[]=4,movie=[]=7... according to how we have sorted them. When this gets rebuilt in php we will have an array that is ordered in the same way as our sorted list with a reference to the index of our movie.

We then make an AJAX call using jQuery's $.get function which sends in our voting submission and waits to get the results back from our php script. Once the results are returned we simply clear out our sortable and insert the results returned from the script.

We start off our php script vote.php with retrieving the values passed in and setting up an array that has a key corresponding to the movie and the voting points allocated to that movie:
$movie_rankings = $_GET['movie'];

if ($movie_rankings && is_array($movie_rankings) && (sizeof($movie_rankings) == 10)) {
 $movie_points = array();
 
 foreach ($movie_rankings as $rank => $index) {
  if (!is_numeric($rank) || !is_numeric($index) || ($rank > 9) || ($index > 10)) exit; // Prevent hacking
  $movie_points[(int)$index] = (10 - (int)$rank);
 } 

You will see I have added a few small security checks in there to avoid someone from screwing this up completely. We begin with getting the value of movie from our $_GET variable. We then check that this wasn't empty, that it is an array and that it has exactly the right number of elements, in our case 10.

Next we create the $movie_points array to hold our votes for each movie. We loop over the rankings passed in and while we are doing this we need to check that someone hasn't just passed in garbage. We check that the $rank and $index are both numbers and that they are both less than 10, another security check. We then set the points for each movie index inside the $movie_points array, casting each value to int in case some tricky person decided to use float values.
$current_rankings = @file_get_contents("rankings.txt");

if ($current_rankings) {
 $current_rankings = unserialize($current_rankings);
 
 foreach ($current_rankings as $index => $score) {
  $movie_points[$index] += $score; 
 }
}

$ser_points = serialize($movie_points);
file_put_contents("rankings.txt", $ser_points); 

Following from that we retrieve the current rankings form our rankings.txt flat file. If the file existed we unserialize those values and loop over each value and add it to the point values just pased in by updating the $movie_points array. We then serialize the $movie_points array and store it in our flat file for later reference.

Now that we have our current standings and all our points tallied we want to build our output. Our output will contain a colourful graphical representation in the form of a bar chart, which will contain the percentages and the names of each of the movies:
arsort($movie_points);
  
$movies = array();
$movies[1] = "The Ten Commandments";
$movies[2] = "Ben-Hur";
$movies[3] = "Planet of the Apes";
$movies[4] = "The Omega Man";
$movies[5] = "Soylent Green";
$movies[6] = "The Three Musketeers";
$movies[7] = "Antony and Cleopatra";
$movies[8] = "Beneath the Planet of the Apes";
$movies[9] = "El Cid";
$movies[10] = "Julius Caeser";

$colours = array();
$colours[1] = "#E52E2E";
$colours[2] = "#E58F2E";
$colours[3] = "#E5DF2E";
$colours[4] = "#8FE52E";
$colours[5] = "#2EE56C";
$colours[6] = "#2EE5D2";
$colours[7] = "#2E49E5";
$colours[8] = "#942EE5";
$colours[9] = "#C02EE5";
$colours[10] = "#DE2EE5";

$total_points = array_sum($movie_points);

$html = '';

$counter = 1;
foreach ($movie_points as $key => $points) {
 $percent = round(($points / $total_points * 100), 0);
 $html .= "<li class='ui-state-default movie-rank non-sortable' id='movie-{$key}'><div style='background:{$colours[$counter]};height:25px;width:{$percent}%;padding:5px;margin-top:-6px;margin-left:-6px;'></div><div style='position:absolute;left:0px;top:0px;'><span class='counter'>{$percent}%</span>{$movies[$key]}</li></div>";
 $counter ++;
}

echo $html;

We begin by sorting our $movie_points from highest to lowest. Then we set up two arrays, one with the index to name mapping of our movies and the next with the colour level for our bar chart. We us php's built in array_sum to count up the total number of points already awarded, this will be used to work out percentages.

Next we loop over (from highest to lowest) each of our movies; we calculate the percentage points and then build the list item displaying our movie name, points and the coloured bar.

This is then echod out and returned to the jQuery scipt which will clear out our voting system and display the colourful results.

That's the lot.

Enjoy!
blog comments powered by Disqus