CakePHP Paginate Multiple Data Sets on One Page

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

 * - $options['url'] Url of the action. See Router::url()
 * - $options['url']['sort']  the key that the recordset is sorted.
 * - $options['url']['direction'] Direction of the sorting (default: 'asc').
 * - $options['url']['page'] Page # to display.
 * - $options['model'] 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).