debuggable

 
Contact Us
 

How to have multiple paginated widgets on the same page with CakePHP

Posted on 25/8/08 by Tim Koschützki

Hey folks,

many of you might have run into the problem of having multiple boxes on the same page that need to be paginated. For example you might have a left column with a list of members of your site and a right column that shows for a example a list of forums. Yeah, that's not the best example, but you get the idea.

So the problem with Cake's paginator is, that its link structure does not know which widget is actually paginated. Let's have a look at a typical url after you clicked a pagination link:

/users/view/488070d4-f5c8-41aa-87aa-4a951490b5da/page:2

Okay great. So imagine we have a UsersController with a view() action and our pagination passes in the page we would like to be on with regards to the list of users. Now, we click a pagination link in the communities box (let's call these boxes "widgets" from now on)... Yes you are right, the link will just look the same. So clicking on the pagination links in the community widget will just paginate our user list and the community list will stay at page 1. : /

Problem

How can we tell the CakePHP paginator which model it shall paginate?

The Solution

There are three things we need for this:

  1. We need to pass in the model that is being paginated to the structure.
  2. We need to ensure we stay on the same page type when we click on a pagination link.
  3. We need to extract the proper page variable from the url depending on the given model and put it into our pagination configuration.

1. Pass in the model that is being paginated to the structure

Okay what I would like to recommend to you here is to have a generic paging.ctp element, which could contain the following code:

if (!isset($paginator->params['paging'])) {
  return;
}
if (!isset($model) || $paginator->params['paging'][$model]['pageCount'] < 2) {
  return;
}
if (!isset($options)) {
  $options = array();
}

?>
<div class="paging">
  <?php
  echo $paginator->prev('<< Previous', array_merge(array('escape' => false, 'class' => 'prev'), $options), null, array('class' => 'disabled'));
  echo $paginator->numbers(am($options, array('before' => false, 'after' => false, 'separator' => false)));
  echo $paginator->next('Next >>', array_merge(array('escape' => false, 'class' => 'next'), $options), null, array('class' => 'disabled'));
  ?>
</div>

By that you ensure that the pagination texts are consistent throughout your application - something that is desirable in most cases. However, the most important reason for this is that we have all the fancy logic to make this hack work in one place and reusable for all widgets.

Here is how you would call the paging element for your Users index list out of your views:

<?php
echo $this->element('paging', array('model' => 'User'));
?>

Let's go over the element's code now. Firstly, we check if we need to do paging - if a model is supplied and if its pageCount is greater than 1. We then supply a link structure for the paging links, including a previous and a next link and numbers. If you have worked with Cake's pagination before, this should be all very familiar to you.

Now on to making our fancy logic work.

if (!isset($paginator->params['paging'])) {
  return;
}
if (!isset($model) || $paginator->params['paging'][$model]['pageCount'] < 2) {
  return;
}
if (!isset($options)) {
  $options = array();
}

$options['model'] = $model;
$options['url']['model'] = $model;
$paginator->__defaultModel = $model;
?>
<div class="paging">
  <?php
  echo $paginator->prev('<< Previous', array_merge(array('escape' => false, 'class' => 'prev'), $options), null, array('class' => 'disabled'));
  echo $paginator->numbers(am($options, array('before' => false, 'after' => false, 'separator' => false)));
  echo $paginator->next('Next >>', array_merge(array('escape' => false, 'class' => 'next'), $options), null, array('class' => 'disabled'));
  ?>
</div>

We are passing the model to the $options array and set a private paginator variable to the $model value. Yeah, that's the ugly part about this hack. However, since we have it only in one place we can live with it. With that code in place, your links render fine and supply the model that needs to be paginated. Our link would now look like:

/users/view/488070d4-f5c8-41aa-87aa-4a951490b5da/page:2/model:User

Keep in mind we have this all configurable when calling the paging element. You can even pass in your own options array to the element if you want to go any further with this.

2. We need to ensure we stay on the same page type when we click on a pagination link

Imagine that you want to display your paginated widgets not only on one page, the users index, but also on /users/view/[ID]. How do we let the paginator know we want to stay on the same user page? Let me rephrase, how do we supply the user ID in a consistent manner to the pagination links, so that if a pagination link in a widget is clicked, it redirects you back to /users/view/[ID], but with the widget being at the proper page.

The code for this is relatively easy, but I am not so happy with the solution yet. What I did is check for a certain variable that would *only* occur on the said pages, like /users/view:

if (isset($community)) {
  $options['url'][] = $community['Community']['id'];
} elseif (isset($user)) {
  $options['url'][] = $user['User']['id'];
}

So, if a $user variable is set, we assume we are on a user specific page. Same for a community... , a forum post, etc.. I feel bad about this part, because that could so easily be buggy. I won't present another approach I had in mind for this, because I would like you guys to discuss the one presented here first. : ]

Here is the pagination element in its entirety:

if (!isset($paginator->params['paging'])) {
  return;
}
if (!isset($model) || $paginator->params['paging'][$model]['pageCount'] < 2) {
  return;
}
if (!isset($options)) {
  $options = array();
}

$options['model'] = $model;
$options['url']['model'] = $model;
$paginator->__defaultModel = $model;

if (isset($community)) {
  $options['url'][] = $community['Community']['id'];
} elseif (isset($user)) {
  $options['url'][] = $user['User']['id'];
}
?>
<div class="paging">
  <?php
  echo $paginator->prev('<< Previous', array_merge(array('escape' => false, 'class' => 'prev'), $options), null, array('class' => 'disabled'));
  echo $paginator->numbers(am($options, array('before' => false, 'after' => false, 'separator' => false)));
  echo $paginator->next('Next >>', array_merge(array('escape' => false, 'class' => 'next'), $options), null, array('class' => 'disabled'));
  ?>
</div>

3. Extract the proper page variable from the url

So it seems we got the frontend part done. Let's have a look at some code for the backend to make this work. For the users list, your code to paginate the users might likely look like this:

$this->paginate['User'] = array(
  'contain' => array('Profile'),
  'order' => array('User.name' => 'asc'),
  'limit' => 20
);
$users = $this->paginate('User');

Straightforward, we need 20 users paginated. Now we need to investigate the url to check if the user model is given in a reusable manner, so that this will work for all paginations of all models:

/**
 * undocumented function
 *
 * @param string $model
 * @return void
 * @access public
 */

  function pageForPagination($model) {
    $page = 1;
    $sameModel = isset($this->params['named']['model']) && $this->params['named']['model'] == $model;
    $pageInUrl = isset($this->params['named']['page']);
    if ($sameModel && $pageInUrl) {
      $page = $this->params['named']['page'];
    }

    $this->passedArgs['page'] = $page;
    return $page;
  }

The trick is this handy little function that you place in your AppController class. It analyzes the url's given model and extracts the page. If the model is not given, it defaults to page 1. By that we ensure the widget that is being paginated is put on the right page and the other ones stay at page 1.

Here is the rewritten controller code:

$page = $this->pageForPagination('User');
$this->paginate['User'] = array(
  'contain' => array('Profile'),
  'order' => array('Profile.name' => 'asc'),
  'limit' => 20,
  'page' => $page
);
$users = $this->paginate('User');

Don't forget to supply the calculated page in your pagination config.

Now you are totally free for the other widgets. For example the pagination code for your community widget would look like this now:

$page = $this->pageForPagination('Community');
$this->paginate['Community'] = array(
  'contain' => false,
  'order' => array('Community.title' => 'asc'),
  'limit' => 5,
  'page' => $page
);
$communities = $this->paginate('Community');

Alrighty, that's it for paginating multiple widgets on the same page with CakePHP. As you see, the hack did not involve a lot of fancy logic, which really is a plus for Cake's paginator. You might wonder why CakePHP does not offer this functionality in the first place. Well, it doesn't yet, but since pagination might be redone in the future chances are good it will support this out of the core at some point.

Also keep in mind we want to promote the use of the controller's $paginate property. In this article I overwrite it for the action used. You might want to move the parameters that aren't changed up to the $params property initialisation.

Have a good one!

-- Tim Koschuetzki aka DarkAngelBGE

 
&nsbp;

You can skip to the end and add a comment.

Neil Crookes said on Aug 26, 2008:

Nice solution Tim. I would make the pageForPagination() function protected to imply that it isn't an action. I wonder what Felix would say to that though.

Neil Crookes said on Aug 26, 2008:

It would be cool to be able to paginate through multiple lists at the same time though wouldn't it? For example, consider a page with 2 columns, left for users, right for forum posts. You paginate through the list of users looking for someone then once you've found the person you're looking for, you can then paginate through his posts. (Another weird example I know, but it could happen... maybe)

Jaime Gómez Obregón said on Sep 17, 2008:

So "page 2 for model A" should look like /page:2/model:A.

But... how should the URL's look like if you want to show page:2 for model A and page 5 for model B?

Jaime Gómez Obregón said on Sep 17, 2008:

I mean, you have to use AJAX for the pagination, because otherwise the table you are not paging will be affected. The only issue is that you cannot freeze (permalink) a given state of the pages, but that should not be a big problem.

Brian  said on Sep 21, 2008:

I'm wondering something similar. How can we sort multiple models at the same time on the same page?

For example, if you have a model Foo that hasMany Bar and Baz records and you want to let the user sort the columns in the html tables showing the Bar and Baz records belonging to a Foo record. Then when you sort on one column in the Bar table and then on a column in the Baz table, both sorts will persist after the page reloads.

Can your solution adapt to allow such a mechanism?

Sanjeev said on Nov 03, 2008:

How can i avoid executing the function using following way
/user/view/page:2/limit:10. I mean i want to avoid the special parameters

I heard that we can set this in our pagination function. but i dont know how exactly to set

Tim Koschützki said on Nov 21, 2008:

@Neil Crookes and Jaime Gómez Obregón : Since you guys propose the same thing, I will answer you both. : ) Yeah well it should be possible to do this. However, I haven't included that in this version of the code. It's going to be interesting - to keep the same pages for different models without using ajax.

Brian: Not at the moment. But I will have a look.

Sanjeev: I don't quite understand what you try to do. Can you explain it some more please?

Sebastien G. said on Mar 25, 2009:

Thanks a lot Tim, was very useful for my quick search field on my home page to search and display multiple models.

Great !

Tim Koschützki said on Mar 25, 2009:

Glad it helped you. :]

Robin  said on Mar 25, 2009:

Hi Tim,

first i want to thank you for this post!
But i still have a problem, because my website is multilingual (english, german) and normally my links should look like: /deu/actual/index

But now with the hack they are going to look like this: /actual/index/page:2/language:deu/model:Project

I tried to fix this by myself, but no chance - iam a cakephp newbie ;-)

can you give me a hint how to change the links back correctly?

Thanks a lot!
Robin

Tim Koschützki said on Mar 25, 2009:

Well in the paging widget you could put in something like

if (isset($url)) {
$options['url'][] = $url;

}

and then pass an url in your paging element and see if you can get it working. :]

Robin  said on Mar 27, 2009:

Hi Tim, thanks for your answer...

but i cant solve the problem anyway.
what ive tried to do is:

$options['model'] = $model;
$options['url']['model'] = $model;

$paginator->__defaultModel = $model;

$options['url'][] = $this->params['language'];
$options['url'][] = $this->params['controller'];

$options['url'][] = $this->params['action'];

$options['url'][] = $this->params['pass'][0];

so, now my url looks this way: /actual/view/deu/actual/view/1/page:2/model:Project/language:deu

you can see i put language/controller/action/param[0] into the link, but its always additional to the first /controller/action and i dont know how to delete this from the link.

Also there is "language:deu" as last parameter given... why the hell? ;-)

I hope you understand my problem...

said on May 02, 2009:

Good morning.
Task: two pagination widgets paginating over Model1 and Model2.

Applied solution: mentioned above (Tim) except the lines in pagination element (i haven't apply them at all):

if (isset($community)) {

$options['url'][] = $community['Community']['id'];

} elseif (isset($user)) {

$options['url'][] = $user['User']['id'];

}

Problem: I have to remember both page number for Model1 and page number for Model 2.

So I did: change the function pageForPagination(Model) to:

pageForPagination(Model, old_page)

where old_page is old page for Model - remembered in session

Still problem: in browser - pagination for Model1 (which is the first one at the code) don't work. (Clicking on it does nothing). And pagination for Model2 alter pages for Model1 and Model2 (eg. in Model2 ->page2, in Model1 ->page2).

What i did: after a lot of tries and errors, in source code of paginate - cake/libs/controller/controller.php line 975:

$options = array_merge($this->params, $this->params['url'], $this->passedArgs);

change:

$options = array_merge($this->params, $this->params['url']);

And it works fine.
My question: is it safe? maybe it will broke sth in future?

Tim Koschützki said on May 04, 2009:

Yeah well, hacking the cake core is never a good idea.

Of the top of my head I feel you should set the session value to this->params['paging'] in the controller, so it is sent to your view.

Maybe you can show your entire code?

said on May 04, 2009:

First of all, i thank you for help. It is not exactly "hack", because (for brevity) i didn't mention that i've copied all paginate function to my app_controller.php. And then changed it.
As far as i know, original paginate function takes the parametr from options supplied in controller ($this->params) and then parametr supplied in url ($this->passedArgs). So, as you say - the best way would be supply in the url all parameters. I would have to recognize which 'number of page' parameter is for which model. Cake don't make it easier and force the format:

.../page:4/model:User (for example). It would be messy, when i would put there my additional paging parameters, i think.

In my controller, i've two functions: yours pageForPagination (a bit changed)
function pageForPagination($model, $page)

{

if(empty($page)) $page=1;

...

, and my, which does whole needed pagination task:

function doPagination($model, $pagination_data,$conditions)
{

$session_page=($this->Session->check("page_$model"))?

$this->Session->read("page_$model"):null;

$page_model = $this->pageForPagination($model, $session_page);

$this->Session->write("page_$model", $page_model);

$this->paginate[$model] = array_merge($pagination_data,

array('page'=>$page_model));

return $this->paginate($model,$conditions);

}

Where $conditions is additional data for finding in model. Then, what doPagination return, i set to view (and then to my pagination element):

$this->set('Model1_data', /* result from pagination*/);
$this->set('Model1_additional_args', /* additional args i need in url */);

The same for model2.
my pagination element takes two arguments: $model - model name, and $additional_args - additional args to paste in url, and then:

...
$options['model'] = $model;

$options['url']['model'] = $model;

$paginator->__defaultModel = $model;

if(!empty($additional_args))
{

if(is_array($additional_args))

{

foreach($additional_args as $arg)

$options['url'][]=$arg;

}

else $options['url'][]=$additional_args;

}

echo $paginator->prev('Previous',
array_merge(

array('escape' => false, 'class' => 'prev'),

$options),

null,

array('class' => 'disabled')

);

... /* numbers, and next in analogous way */

On the other hand, if i would somehow manage all these parameters in url, remembering the numbers of pages in session will not be needed.

cheers,
m

Chris  said on Jun 15, 2009:

The params not work for me... they are only available in the controller that is requested.. changed it to this:

[CODE]
$params = Dispatcher::parseParams(Dispatcher::uri());

$sameModel = isset($params['named']['model']) && $params['named']['model'] == $model;

$pageInUrl = isset($params['named']['page']);

if ($sameModel && $pageInUrl) {
$page = $params['named']['page'];

}

$this->passedArgs['page'] = $page;
return $page;

[/code]

And it works... but still need to fix sort.. cause it ignores the model

Daniel Voyce  said on Jul 13, 2009:

Hi Chris,

Could you post your code in its entirity please? I cant work out which part you have replaced with what. I have a page with 8 different widgets on it and trying to paginate them all independently

Dan

Chris  said on Jul 13, 2009:

I changed the code that goes in appmodel.. function pageForPagination

I dont have the code anymore don't ask.. hehe but am pretty sure it went like this.

function pageForPagination($model) {
$page = 1;

$params = Dispatcher::parseParams(Dispatcher::uri());

$sameModel = isset($params['named']['model']) && $params['named']['model'] == $model;

$pageInUrl = isset($params['named']['page']);

if ($sameModel && $pageInUrl) {
$page = $params['named']['page'];

}

$this->passedArgs['page'] = $page;
return $page;

}

Daniel Voyce  said on Jul 13, 2009:

Ok im with you now :) Did you get it working in the end?

thanks for the quick reply dude!

PRIDGIGULNERN said on Nov 17, 2009:

Bonjour, debuggable.com!
http://meds.fora.pl/>Acheter du viagra http://medsonline.fora.pl/>Acheter du viagra http://onlinefarmacia.fora.pl/> viagra en ligne http://masar.fora.pl/> viagra online http://med.fora.pl/>Acheter du viagra online

HockBoord said on Nov 20, 2009:

Hello
http://www.geomaticsoman.com/ - buy generic ambien

Most insomniacs would be happy to know that there is a drug called Ambien (Zoldipem) which can treat their insomnia.
http://www.geomaticsoman.com/>generic ambien

Save big money ordering Ambien (Zolpidem) in bulk.

http://www.geomaticsoman.com/>purchase ambien

Most insomniacs would be happy to know that there is a drug called Ambien (Zoldipem) which can treat their insomnia.

Vinod  said on Nov 23, 2009:

Hello Tim,
Great article. But I have some different problem, I need to use multiple pagination widget on same page for same 'Model'. So could you please suggest any way how to achieve this.

Tim Koschützki said on Nov 23, 2009:

Hey Vinoid,

well this is very hard as Cake uses the pagination parameters to build the pagination links. Your second call to controller->paginate() would overwrite these parameters. Without a bigger core hack this will be very hard to do.

What's your usecase? Maybe you could look at ajaxing all of these pagination lists, so that you only have to paginate one list at the same time.

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.