Bugs with filtering paginated content on a page

search

#1

Hi, I’ve been cracking my head on this problem for a while now. I’m building a site for a wine merchant. He has a lot of varieties that have the following properties that can be set in the backend:

  • estate (type: select w/ query)
  • kind (type: tags)
  • grape (type: tags)
  • region (type: tags)
  • price (type: select)

Based on Filtering Content on a page - I’ve been able to get the basic functionality working: frontend shows filters based on the content in the backend and all wines show, including pagination.

Usage example:
A page shows the full assortment of wines, including pagination. When a filter is applied using the select boxes the page refreshes and returns all wines that match that criteria.

I run into the following problems on the acceptance environment https://vinura.olafmuller.nl/assortiment

  1. When a filter is applied (example: Estate A) and the user paginates, the page refreshes, the pagination occurs, however, the filters are reset and the full assortment is shown again. We want to paginate with the query staying intact.
  2. When no filters are applied and the last page is opened (example: page 6) after applying a filter (example: rood), the page refreshes and opens the last page of wines using the new query; this should be the first page. After paginating again, the first bug occurs again.
  3. I’d like the filter options to be sorted alphabetically, I’ve been experimenting but filters are objects in an array, and with my limited PHP knowledge, I’ve not been able to get it to work.

The products.php controller:

<?php

return function ($kirby) {

	$args = $kirby->route()->arguments();

	$products = page('assortiment')->children()->listed();

	$estate = $products->pluck('estate', null, true);
	$kind = $products->pluck('kind', null, true);
	$grape = $products->pluck('grape', null, true);
	$region = $products->pluck('region', null, true);
	$price = $products->pluck('price', null, true);

	$keys = array('estate', 'kind', 'grape', 'region', 'price');

	// return all children if nothing is selected
	$projects = $products;

	// if there is a post request, filter the projects collection
	if (r::is('POST') && $data = get()) {
		$projects = $products->filter(function ($child) use ($keys, $data) {

			// loop through the post request
			foreach ($data as $key => $value) {

				// only act if the value is not empty and the key is valid
				if ($value && in_array($key, $keys)) {

					// return false if the child page's category and value don't match
					if (!$match = $child->$key() == $value) {
						return false;
					}
				}
			}

			// otherwise return the child page
			return $child;
		});
	}

	$projects = $projects->shuffle()->paginate(3);
	$pagination = $projects->pagination();

	return compact('projects', 'estate', 'kind', 'grape', 'region', 'price', 'data', 'pagination');
};

The input and output of the products.php template:

<div class="grid">
	<aside class="cell cell--1 cell--small-1/3 cell--medium-1/4 is-sticky">
		<h2 class="visually-hidden">Filters</h2>

		<form id="filters" method="post" class="section section--sticky">
			<div class="field">
				<label for="estate" class="field__label">Estate</label>
				<select id="estate" class="field__input" name="estate" onchange="this.form.submit()">
					<option selected value="">Selecteer een estate</option>

					<?php foreach($estate as $item) : ?>
						<?php if(!$item) continue ?>

						<option<?php e(isset($data['estate']) && $data['estate']==$item, ' selected' ) ?> value="<?= $item ?>">
							<?= page($item)->title() ?>
						</option>
					<?php endforeach ?>
				</select>
			</div>

			<div class="field">
				<label for="kind" class="field__label">Soort</label>
				<select id="kind" class="field__input" name="kind" onchange="this.form.submit()">
					<option selected value="">Selecteer een soort</option>

					<?php foreach($kind as $item) : ?>
						<?php if(!$item) continue ?>

						<option<?php e(isset($data['kind']) && $data['kind']==$item, ' selected' ) ?> value="<?= $item ?>">
							<?= $item ?>
						</option>
					<?php endforeach ?>
				</select>
			</div>

			<div class="field">
				<label for="grape" class="field__label">Druivenras</label>
				<select id="grape" class="field__input" name="grape" onchange="this.form.submit()">
					<option selected value="">Selecteer een druivenras</option>

					<?php foreach($grape as $item) : ?>
						<?php if(!$item) continue ?>

						<option<?php e(isset($data['grape']) && $data['grape']==$item, ' selected' ) ?> value="<?= $item ?>">
							<?= $item ?>
						</option>
					<?php endforeach ?>
				</select>
			</div>

			<div class="field">
				<label for="region" class="field__label">Regio</label>
				<select id="region" class="field__input" name="region" onchange="this.form.submit()">
					<option selected value="">Selecteer een regio</option>

					<?php foreach($region as $item) : ?>
						<?php if(!$item) continue ?>

						<option<?php e(isset($data['region']) && $data['region']==$item, ' selected' ) ?> value="<?= $item ?>">
							<?= $item ?>
						</option>
					<?php endforeach ?>
				</select>
			</div>

			<input type="submit" value="Filter" class="button button--large button--primary input-not-mandatory trailer">
		</form>
	</aside>

	<div class="cell cell--1 cell--small-2/3 cell--medium-3/4">
		<h2 class="visually-hidden">Overzicht</h2>

		<?php if($projects->count()) : ?>
		<div class="grid">
			<?php foreach($projects as $item) : ?>
			<div class="cell cell--1 cell--small-1/2 cell--medium-1/4">
				<?php snippet('card-wine', array('item' => $item)) ?>
			</div>
			<?php endforeach ?>
		</div>
		<?php else : ?>
		<span class="header header--s trailer text-middle is-subjacent">Geen resultaten gevonden die voldoen aan de opgegeven filters.</span>
		<?php endif ?>

		<?php if($projects->pagination()->hasPages()) : ?>
		<nav class="trailer">
			<ul class="pagination">
				<?php if($pagination->hasPrevPage()) : ?>
				<li class="pagination__item"><a class="pagination__link button button--contained" href="<?= $pagination->prevPageURL() ?>">Vorige</a></li>
				<?php endif ?>

				<?php foreach($pagination->range(10) as $r): ?>
				<li class="pagination__item">
					<a class="pagination__link button button--contained<?php if($pagination->page() == $r) echo ' is-active' ?>" href="<?= $pagination->pageURL($r) ?>">
						<?= $r ?>
					</a>
				</li>
				<?php endforeach ?>

				<?php if($pagination->hasNextPage()): ?>
				<li class="pagination__item"><a class="pagination__link button button--contained" href="<?= $pagination->nextPageURL() ?>">Volgende</a></li>
				<?php endif ?>
			</ul>
		</nav>
		<?php endif ?>
	</div>
</div>
```

Edit: I've also added the question about alphabetical sortation of filters.

#2

This sounds like an issue i helped with some months ago. I think the answer was to use Ajax, because when you apply the filter its running through the whole list again and resetting the pagination because you now have a new list, it doesnt apply the filtering to the existing list. You need to store the previous result and apply the filter to that, not the original collection.

Having built sites for winemakers myself, I know this can be a bit of a pain :slight_smile:

I would probably get all the wine data upfront in json or something and pump it something like list.js to deal with the filtering.


#3

The easiest way is to use a GET request instead of a POST by changing the method in your form and removing the check for POST request in your controller. There is absolutely no reason to send a POST request for this filtering stuff.

If you want to keep the POST request for some reason, you would have to perpetuate your filter data in a session cookie.

This works completely without Ajax or any other JavaScript.


#4

Hi Texnixe, this solution works like an absolute charm and fixes problem 1 and the last part of problem 2. Would you please be so kind as to also help me with the other problems stated above? I’ve done a var_dump($region) and this shows up:

array(3) { [0]=> object(Kirby\Cms\Field)#341 (1) { ["region"]=> string(11) "Zuid-Afrika" } [1]=> object(Kirby\Cms\Field)#344 (1) { ["region"]=> string(12) "Noord-Afrika" } [2]=> object(Kirby\Cms\Field)#347 (1) { ["region"]=> string(11) "Oost-Afrika" } }

A friend told me that to display the filters alphabetically, I should use the usort function? Something like:

usort($array, 'sortByRegion');
function sortByRegion($a, $b) {
	return strcmp($a['region'], $b['region']);
}

Edit: I’ve uploaded the changes mentioned by Texnixe: https://vinura.olafmuller.nl/assortiment - now the URL also updated; perfect.

Not relevant, but for the record: I’d like to apologize for my lack of PHP knowledge and dumping the problem here. The Kirby forums have always been a great solution for most of the issues I encounter. I love working with Kirby and the quick replies seen above once again show why I use Kirby for all my freelance work!


#5

I usually use a separator in the pluck() method (no matter if the categories actually contain one or not:

$grape = $products->children()->pluck('grape', ',', true);
sort($grape);
dump($grape);

The result is a simple array I can then sort.

As regards the first part of issue 2, I must admit that I don’t quite get it and don’t know how to reproduce it.


#6

That did the trick, thanks so much for your time and effort. I’ll make sure to buy all future licenses using your affiliate link :wink:!

@jimbobrjames Also thank you for your contribution, I’m glad there’s a more simple solution from what I gather, seeing my PHP and JS knowledge isn’t all that great. But again, thanks for the answer!


#7

@Olaf_Muller What about the remaining issue? How can we reproduce that?


#8

How to reproduce:

  1. Go to the product overview
  2. Paginate to the last page
  3. From the Estate filter selection box, choose option: Estate A

This results in a perfect filtered page, but as you can see in the URL the page is still on page 6 instead of the intended page 1.

Here’s the breakdown of the URL, hope this clarifies:

URL: [...]/assortiment/page:6[...]
                       ^should be page:1 because we applied a new filter

#9

I’m not quite sure what the issue is, but let’s fix some obvious stuff first.

This should be $pagination->hasPages()

The range should use the $limit variable, not a fixed value.