Kayak-like filter sliders using jQuery and AJAX pagination in CakePHP

Let’s try to build a nice slider widget to filter your data… kind of like the ones you’d find on kayak.com (do a search for any flight to see a sample).

First we need to enable our data to be paginated via AJAX.
For this, I am relying on an excellent and very simple technique, provided by Casey Dreier. It really only takes a few minutes to setup and works wonderfully well with CakePHP built-in Paginator.

You should probably setup AJAX pagination for your data first, but if you are interested in just adding the slider filter functionality, here’s how to do it…

Let’s say we are building a real estate web site where on a page we show a bunch of listings of various properties for a given town (My Little Town). Now, we’d like to let the user to easily filter the price range by using a cool jQuery slider.

Going a bit backwards, perhaps, we’ll start with the view…

This is how I’ve got everything setup:

<?php
   $html->css('data_grid', null, null, false);
    $html->css('search_filters', null, null, false);
    $html->css('jquery_ui_blue', null, null, false);

    $javascript->link('jquery/jquery.min', false);
    $javascript->link('jquery/jquery_ui.min', false);
    $javascript->link('jquery/ui/jquery.slider.min', false);

    $javascript->link('jquery/page_specific/towns_view', false);
?>

<div class="page-title"><?php echo !empty($searchString) ? Inflector::humanize($searchString) : null; ?></div>

<form>
    <input type="hidden" id="search-string" value="<?php echo $searchString; ?>" />
    <input type="hidden" id="max-price" value="<?php echo $maxPrice['Property']['sale_price']; ?>" />
    <input type="hidden" id="min-price" value="<?php echo $minPrice['Property']['sale_price']; ?>" />
</form>

<p>
   <div class="search-filters">
       <form>
           <?php echo $form->label('amount', 'Filter by price:'); ?>
           <?php echo $form->text('amount', array('id'=>'amount', 'class'=>'amount-holder')); ?>
       </form>
       <p>
           <div id="slider"></div>
       </p>
   </div>
</p>

<div id="listing-data"><?php echo $html->image('icons/loading.gif'); ?></div>

First, we include our relevant CSS files and jQuery, that’s a no-brainier.

Inflector::humanize($searchString) will convert our search string, which usually comes from the URL (i.e. www.example.com/towns/view/my_little_town) into “My little town”.

Then we’ve got a bunch of hidden fields for the JavaScript to get the values from. As you’ve guessed some of these are used as settings for the slider as well as let our JS grab the search string, which will be sent to the server via AJAX.

Then we’ve got our prepared filter with the slider, and finally the div, which is going to hold the listing data (all of the details of the listing data setup are explained in the article linked above).

As you can imagine we set the min and max prices in the controller (as well as the search string)…

Now, let’s take a look at our simple jQuery script that will handle data retrieval and filtration for us:
(That’s the file towns_view.js, which is linked above)

[sourcecode language=”javascript”]
$(document).ready(function(){
var minPrice = $(‘#min-price’).val();
var maxPrice = $(‘#max-price’).val();
var searchString = $(‘#search-string’).val();

minPriceInt = parseInt(minPrice);
maxPriceInt = parseInt(maxPrice);

loadPiece(‘/towns/get_listings/’ + searchString, ‘#listing-data’);

$(“#slider”).slider({
range: true,
min: minPriceInt,
max: maxPriceInt,
values: [minPriceInt, maxPriceInt],
step: 100,
slide: function(event, ui){
$(“#amount”).val(‘$’ + ui.values[0] + ‘ – $’ + ui.values[1]);
},
stop: function(event, ui){
loadPiece(‘/towns/get_listings/’ + searchString + ‘/minPrice:’ + $(“#slider”).slider(“values”, 0) + ‘/maxPrice:’ + $(“#slider”).slider(“values”, 1), ‘#listing-data’);
}
});

$(“#amount”).val(‘$’ + $(“#slider”).slider(“values”, 0) + ‘ – $’ + $(“#slider”).slider(“values”, 1));

});

function loadPiece(href, divName){

$(divName).load(href, {}, function(){
var divPaginationLinks = divName + ‘ .paginator-links a';

$(divPaginationLinks).click(function(){
var thisHref = $(this).attr(‘href’);
loadPiece(thisHref, divName);
return false;
});

});
[/cc]

First, we prepare our script with the values we’ve gotten from the hidden fields of our view…


var minPrice = $(‘#min-price’).val();
var maxPrice = $(‘#max-price’).val();
var searchString = $(‘#search-string’).val();

The loadPiece(), which was kindly posted at the above article, handles loading paginated data via AJAX into our “listing-data” div (remember the view?)…

So, on the first page load loadPiece() will grab all paginated data for a given model, using the $searchString (which originally came from the URL, and then was set to a hidden field).

The minPrice and maxPrice values are obtained from the controller (in our case it would be the minimum price of some property for sale, or the maximum price).

Without going into too much detail here’s an easy way to grab that info from your database.

This is done and set in some action of the controller, perhaps view(), following our example…

$this->set('maxPrice', $this->Property->find('first', array('fields'=>'Property.sale_price',
                                                                             'order'=>array('Property.sale_price DESC'),
                                                                             'conditions'=>array('Property.city LIKE' => '%'.$searchString.'%'))));

$this->set('minPrice', $this->Property->find('first', array('fields'=>'Property.sale_price',
                                                                            'order'=>array('Property.sale_price ASC'),
                                                                            'conditions'=>array('Property.city LIKE' => '%'.$searchString.'%'))));

I hope this code is self-explanatory.

Now that we know and have set our $minPrice and $maxPrice, we are ready to go ahead and setup the slider…

Note:

minPriceInt = parseInt(minPrice); and maxPriceInt = parseInt(maxPrice); this was necessary for the slider to work, because otherwise the values were treated as a string and broke the slider…

Alright, I’m not really going to go in depth about the setup of the slider as it is quite nicely covered in the jQuery examples and the manual, but will point out a few things:

  1. values: [minPriceInt, maxPriceInt] – means where the slider handles are originally positioned, in this case I want them both to be at the min and max coordinates respectively
  2. step: 100 – means how many units the slider handle jumps, so in case of our sale prices… it means 100 bucks on each slide
  3. slide – provides the user with the feedback on what price range is currently selected. You can see this code and how it looks in action right here
  4. stop – is our main “filtering” function it takes the values of the price range from the slider handle’s position and, again, using the loadPiece() function gets the paginated data from the server. Only this time, of course, the prices are passed to the ‘conditions’ array of the find() method and therefore the list becomes filtered out

Our method, the one triggered by an AJAX request, might look something like this:

function get_listings($searchString = null) {

Configure::write('debug', 0);

if($searchString) {

 $searchConditions = array('Property.city LIKE' => '%'.$searchString.'%');

 if($this->params['named']['maxPrice'] || $this->params['named']['minPrice']) {
    $searchConditions = Set::merge($searchConditions, array('Property.sale_price <=' => $this->params['named']['maxPrice'],
                                                                            'Property.sale_price >=' => $this->params['named']'minPrice']));
  }

  $this->set('listings', $this->paginate('Property', $searchConditions));
  }
}

Well, that’s all there is to it. I know this post doesn’t go into a lot of detail and code specifics, but it should give you a good general idea on how to setup such a filter with AJAX. Definitely try out the method described above to do an AJAX pagination, it is simple, effective and allows you to create really cool interactive widgets like this one for your data filters.

Plus working with AJAX and jQuery is always “fun” :)

m4s0n501
  • fly2279

    Has anyone had trouble using the code from the bakery article multiple times in one page? I’m trying to use it to load three divs with different information in each one. the loadPiece code works but sometimes all three load correctly and sometimes only one loads and the others fail. Any ideas?

  • http://teknoid.wordpress.com teknoid

    @fly2279

    Use firebug to see the response returned from the server. That should give you a hint as to what’s going on.

    • fly2279

      The response is blank on two out of three. I’m using the same function with passed variables to differentiate the conditions for each of the three pagination divs. i.e. /model/condition1/id1, /model/condition2/id1, etc. In the controller I use the condition to paginate the same model three times with three sets of conditions. I’ve even tried using three different functions in the controller with three different views to load them just to try to eliminate problems but I get the same results.

  • http://teknoid.wordpress.com teknoid

    @fly2279

    Try to check your SQL to see what is supposed to be returned. (If it works once, it should work for other cases… if all conditions are met).

  • fly2279

    The weird thing is that you can refresh it and it will have a response (and load) all three, other times it will load one or two of the three sets randomly. Each of the three have a result set but they just aren’t getting returned to be loaded.

    • fly2279

      I finally gave up and just uploaded to the server to see if it was something in the local install. It works perfectly on the server. Thanks for helping me troubleshoot.

  • http://teknoid.wordpress.com teknoid

    @fly2279

    No problem. Glad you’ve got it resolved.