CakePHP Paginate Multiple Data Sets on One Page

by frank on November 17, 2008

in Featured, PHP

Wanted: a simple method to paginate multiple sets of data separately on a single page using the cakePHP paginator helper/object.

The problem with standard cakePHP pagination

In this case I want to paginate the news items, but I have a flag in the news item table for is_archive, so basically want to seperate the current news items from the archived ones and display both sets of data on the same page in two seperate tables. Then use cakePhp’s built in pagination to navigate through both sets of data independently of each other.

I used AJAX and the paginator options to get a very simple implementation working quite quickly – its not a perfect solution and has some limitations to do with the total number of pages in each dataset that you can paginate to.


Some useful CakePHP paginator options

Using the options of the paginator helper and AJAX you should be able to paginate several different sets of data for several different models on a single page:

/**
 * Holds the default options for pagination links
 
 * - <i>$options['url']</i> Url of the action. See Router::url()
 * - <i>$options['url']['sort']</i>  the key that the recordset is sorted.
 * - <i>$options['url']['direction']</i> Direction of the sorting (default: 'asc').
 * - <i>$options['url']['page']</i> Page # to display.
 * - <i>$options['model']</i> The name of the model.
 * ...(+ more options)
 */

In this case I just use the url option in order to retrieve the correct data for each seperate pagination. But if you wanted to paginate two seperate models on a single page you could pass the model option in theory (I didn’t need to test this).

Paginate in the controller

function index() {
	$this->News->recursive = 0;
 
	$this->set('news', $this->paginate(null, array('is_archive'=>0)));
	$this->set('archive', $this->paginate(null, array('is_archive'=>1)));
}

Paginator helper in the view

Simply get the two sets of data and pass them to the View:

<div id="news_data">
<div class="news index">
<h2><?php __('News');?></h2>
 
<table cellpadding="0" cellspacing="0">
<tr>
	<th><?php echo $paginator->sort('id', null, array('url'=>array('action'=>'get_news')));?></th>
	<th><?php echo $paginator->sort('title', null, array('url'=>array('action'=>'get_news')));?></th>
	<th><?php echo $paginator->sort('date', null, array('url'=>array('action'=>'get_news')));?></th>
	<th><?php echo $paginator->sort('created', null, array('url'=>array('action'=>'get_news')));?></th>
	<th class="actions" colspan="2"><?php __('Actions');?></th>
</tr>
<?php
$i = 0;
foreach ($news as $news):
	$class = null;
	if ($i++ % 2 == 0) {
		$class = ' class="altrow"';
	}
?>
	<tr<?php echo $class;?>>
		<td>
			<?php echo $news['News']['id']; ?>
		</td>
		<td>
			<?php echo $news['News']['title']; ?>
		</td>
		<td>
			<?php echo $news['News']['date']; ?>
		</td>
		<td>
			<?php echo $news['News']['created']; ?>
		</td>
		<td class="actions">
    		<?php echo $html->link($html->image('up.gif'), array('action'=>'moveup', $news['News']['id']), array('title'=>'up'), false, false); ?>
            <?php echo $html->link($html->image('down.gif'), array('action'=>'movedown', $news['News']['id']), array('title'=>'down'), false, false); ?>
		</td>
		<td class="actions">
			<?php echo $html->link(__('View', true), array('action'=>'view', $news['News']['id'])); ?>
			<?php echo $html->link(__('Edit', true), array('action'=>'edit', $news['News']['id'])); ?>
			<?php echo $html->link(__('Delete', true), array('action'=>'delete', $news['News']['id']), null, sprintf(__('Are you sure you want to delete news item: "%s"?', true), $news['News']['title'])); ?>
		</td>
	</tr>
<?php endforeach; ?>
</table>
</div>
<div class="paging">
    <div class="paging_left">
        <?php echo $paginator->counter(array('format' => __('Page %page% of %pages%', true)));?>
    </div>
    <div class="paging_right">
    <?php echo $paginator->prev('<< '.__('previous', true), array('url'=>array('action'=>'get_news')), null, array('class'=>'disabled'));?>
 |  <?php echo $paginator->numbers(array('url'=>array('action'=>'get_news')));?>
    <?php echo $paginator->next(__('next', true).' >>', array('url'=>array('action'=>'get_news')), null, array('class'=>'disabled'));?>
    </div>
</div>
</div>

This is the portion of the view where I’m displaying the current news items. It differs only slightly from the standard cakePHP index view, firstly I wrapped all the data I want to substitue in a div:

<div id="news_data"></div>

I’ve also changed the paginator links by passing the url option:

<?php echo $paginator->numbers(array('url'=>array('action'=>'get_news')));?>

Now all I do is bind the click events of the paginator links to a javascript function which performs an AJAX call, retrieving the data from the url and substituting the content of:

<div id="news_data"></div>

with the html passed back from the AJAX call.

The javascript to return the pagination results

Using jQuery for this:

$(document).ready(function () {
 
    $('.paging a').click(paginate);
    $('th a').click(paginate);
 
});
 
var paginate = function(event) {
    event.preventDefault();
 
    var href;
    href = $(this).attr('href');
 
    $.ajax({
      url: $(this).attr('href'),
      cache: false,
      success: function(html){
 
        if (href.match(/.*\/get_archive\/.*/i)) {
            $('#archive_data').html(html);
        }
        if (href.match(/.*\/get_news\/.*/i)) {
            $('#news_data').html(html);
        }
 
        $('.paging a').click(paginate);
        $('th a').click(paginate);
 
      }
    });
}

So I bind all the anchor tags of the paging div and all the anchor tags inside the th cells so they all make AJAX calls for pagination. The href.match is because I have the archive news item data in the same view paginating in the same way.

Back to the controller to paginate the data

So, the controller code for handling these AJAX calls is straightforward, just returning the paginated data of the news items:

function get_news() {
    	if ($this->RequestHandler->isAjax()) {
             $this->set('news', $this->paginate(null, array('is_archive'=>0)));
             $this->viewPath = 'news/ajax/';
        }
}

The View just returns a big chunk of html identical to the view above, so its not very light and quick. But its about as straightfroward as you can get :-)

HTH.

NOTE:
I have come across a limitation of this technique which I haven’t had time to look into – if the paginator object is shared between the two sets of data on the page (when you load index view) then it shares attributes.

So the archive data might paginate to several pages, while the news might only have a couple of items in there, BUT, the news paginator will show pagination links to several pages…or so it seems (I’ve just noticed this).

Was this article useful?

rss feed icon

Email this article to yourself or...

rss feed icon

Subscribe to the RSS feed for more useful articles and tips.

Share this article with others

  • del.icio.us
  • Twitter
  • Reddit
  • StumbleUpon
  • Facebook
  • Digg
  • http://www.mistral-tech.pl Mistral

    isn’t it easier jsut to do it in “plain” php? :-)

  • http://deadlytechnology.com franktank

    well, possibly! the cakephp built in pagiantion has proved very useful however, just in this case I wanted to paginate two sets of data on the same page which is a bit odd.

    if you were to tackle this without using the built in pagination then you would have to juggle lots of variables around in the URL I would think which gets messy when you are editing data etc.

  • saintberry

    In the JS rather than rebinding the events you could just use jQuery.live()

  • frank

    @saintberry good tip! thanks

  • Jacques

    Hi There,

    Did you find a work around for the limitation you mentioned above? (Separating the shared paginator attributes?)

  • frank

    No I didn’t, though I didn’t look very hard its not obvious how to get around that limitation from memory.

  • Jacques

    Well my turn to try … should be fun :-)

  • Jacques

    And well although not that much fun my solution was quick and simple.

    moved $this->set(‘archive’, to a secondary method, then called that using javascript->event and ajax->remotefunction to update the second element on the page load.

    And all is hunky dory.

    By the way, thanks for the article above.

  • frank

    no probs, thanks for the solution

  • http://cake-php.net wizard

    in your script you will have a wrong count of pages.

    This topic is useless, the problem ‘CakePHP paginator for multiple sets of data on one page using AJAX’ is not solved

  • frank

    Yes, I think it will be the wrong count of pages for the set of data with the least number of pages (from memory). I think Jacques got around this issue using a separate AJAX call to update the page with the correct count of the smallest data set.

  • pere

    sorry jacques but that is just not a solution. that is crap. no offense, but googling this problem for hours and then finding this, just makes me wanna cry.

  • frank

    @pere I’m concerned that it took hours to find this post on google. This post doesn’t so much solve the problem of multiple pagination but it provides an interesting approach and highlights some limitations with the cakephp paginator.

  • http://www.nickfehr.com Nick

    Do you know of a non-AJAX solution for this?

  • frank

    Sorry Nick, I don’t

  • http://www.facebook.com/binoyav Binoy Av

    Hi,

    I have a similar requirement. But instead of news and archives I have a dynamic set of data. For example, I need to show the category (as a title) and products of that category with pagination. Below
    that I need to show the next category with the products and pagination etc etc. How can i do this ?

  • Anonymous

    Not sure, I think you’re going to have to figure out what sets of data you are going to display in the controller then pass it to the view in some kind of array and loop over.