Filter project-list by multiple custom fields

Hi my name is Jack, I’m a student and an absolute beginner on php. Please be easy on me : ) I worked quite a bit with Wordpress but two weeks ago I found Kirby and today I purchased a license because even I can tell that it’s awesome!

Here is my problem: I want a filterable list of projects I did at university. Here is a sketch:


So I have a list of projects, with custom fields (status, year, category…) and I want to filter them with drop-down menus.

Canceled + Animation gives you Project 6
In Progress + Drawing gives you Project 2
or
Completed + 2013 gives you Project 4 and Project 5

I managed to get drop-down menus with my custom fields but I don’t know how to filter my projects through them.

A push in the right direction would me highly appreciated!

You could use a filter with a callback, something like:

<?php
$projects = $page->children()->visible();

if($_POST) {
  $projects = $page->children()->visible()->filter(function($child) {
    foreach ($_POST as $key => $value) {
      if ($value != '') {
        if (! $match = $child->$key() == $value) {
	  return false;
        }
      }
    }
    return $child;
  });
}

Hi texnixe, thanks for your reply! Unfortunately that’s way over my head :frowning:
Shall I put this in controllers/projects.php ?

What would I put in my template? Currently I have this:

  <?php foreach(page('projects')->children()->visible() as $project): ?>
   <a href="<?php echo $project->url() ?>"> / <?php echo $project->numb()->html() ?>  <?php echo $project->title()->html() ?> / <?php echo $project->status()->html() ?>  / <?php echo $project->year()->html() ?>  / <?php echo $project->cat()->html() ?></a>
  <?php endforeach ?>

Which gives me my project list like this : 00 Projectname / Status / Year / Category
But how would I control your filter?

Sorry I’m really new to this and I know these are very very basic questions.

Exactly, put the above code in a projects.php controller:

return function($site, $pages, $page) {

 //fetch all projects
 $projects = $page->children()->visible();

//check for $_POST request
 if($_POST) {
  //filter the projects
   $projects = $page->children()->visible()->filter(function($child) {
     //loop through all items in the $_POST array
     foreach ($_POST as $key => $value) {
       if ($value != '') {
         // check if the child matches a value, $key() is a replacement for the fields, i.e. category
         if (! $match = $child->$key() == $value) {
	   return false;
         }
       }
     }
     // return all matches
     return $child;
   });
 }
  return compact('projects');

};

The in your template, replace page('projects')->children()->visible() with $projects:

 <?php foreach($projects as $project): ?>
   <a href="<?php echo $project->url() ?>"> / <?php echo $project->numb()->html() ?>  <?php echo $project->title()->html() ?> / <?php echo $project->status()->html() ?>  / <?php echo $project->year()->html() ?>  / <?php echo $project->cat()->html() ?></a>
  <?php endforeach ?>

This is a pure PHP solution, for this to work, you would need a submit button.

An alternative would be to send your request through AJAX, or use a pure Javascript client-side filtering solution like Isotope or Shuffle.js.

Hi texnixe I highly appreciate your help!

I put your code in my controller but now my projects page goes blank.

Do I need a submit button? Since I want to use a drop-down-menu as my filter controller I could use something like this?

  <?php
    // filter options
    $items = $projects->visible();
    // only show options if items are available
    if($items->count()):
    ?>
        <select onchange="this.form.submit()">
          <?php foreach($items as $item): ?>
          <option value="<?php echo $item->status()->html() ?>"><?php echo $item->status()->html() ?></option>
          <?php endforeach ?>
        </select>
    <?php endif ?>

Here is everything I’ve got so far:

BLUEPRINT

<?php if(!defined('KIRBY')) exit ?>

title: Project
pages: false
files:
  sortable: true
fields:
  id000:
    label: Project-ID
    type: number
    width: 1/5
  title:
    label: Project-Name
    type:  text
    width: 4/5
  year:
    label: Year
    type: date
    format: YYYY
    width: 1/3
  status:
    label: Status
    type: select
    options:
      completed: Completed
      progress: In Progress
      canceled: Canceled
      concept: Concept 
    width: 1/3
  cat:
    label: Category
    type: select
    options:
      cat-1: Category 1
      cat-2: Category 2
      cat-3: Category 3
      cat-4: Category 4
    width: 1/3
  text:
    label: Text
    type:  textarea
  tags:
    label: Tags
    type:  tags

CONTROLLER

<?php
return function($site, $pages, $page) {

 //fetch all projects
 $projects = $page->children()->visible();

//check for $_POST request
 if($_POST) {
  //filter the projects
   $projects = $page->children()->visible()->filter(function($child) {
     //loop through all items in the $_POST array
     foreach ($_POST as $key => $value) {
       if ($value != '') {
         // check if the child matches a value, $key() is a replacement for the fields, i.e. category
         if (! $match = $child->$key() == $value) {
	   return false;
         }
       }
     }
     // return all matches
     return $child;
   });
 }
  return compact('projects');

};

TEMPLATE

<?php

// filter options
$items = $projects->visible();

// only show options if items are available
if($items->count()):

?>
<select onchange="this.form.submit()">
  <?php foreach($items as $item): ?>
  <option value="<?php echo $item->html() ?>"><?php echo $item->title()->html() ?></option>
  <?php endforeach ?>
</select>

<?php endif ?>


  <?php foreach($projects as $project): ?>
   <a href="<?php echo $project->url() ?>"> 
   /
    <?php echo $project->id000()->html() ?>
	<?php echo $project->title()->html() ?> / 
	<?php echo $project->status()->html() ?>  / 
	<?php echo $project->year()->html() ?>  / 
	<?php echo $project->cat()->html() ?>
    </a>
  <?php endforeach ?>

PS. I’m absolutely aware that this problem is beyond your support – all the more I would be happy to get this working : )

Please turn on debugging in your config.php

c::set('debug', true);

I just spotted a stray single quote in the return function of the controller and corrected it in my code above. Maybe that is already the reason why it does not work as expected.

Also, your first line in your template should be changed to

$items = page('projects')->children()->visible();

Otherwise you only get the options from the filtered projects if the projects are filtered.

Ok, debug is on and I updated your code.

  <?php foreach($projects as $project): ?>
   <a href="<?php echo $project->url() ?>"> / <?php echo $project->id000()->html() ?>  <?php echo $project->title()->html() ?> / <?php echo $project->status()->html() ?>  / <?php echo $project->year()->html() ?>  / <?php echo $project->cat()->html() ?></a>
  <?php endforeach ?>

Now I get a complete list of projects like this (I will clean this up later)
1312 Project A / concept / 1995-01-01 / cat-3 /

But how do I control the filter through a select field?

You can actually do it like you suggested above: onchange:'this.form.submit()', provides your select fields are wrapped within a form

<form id="filters" action="" method="post">
	//your select fields
</form>

Instead of using onchange you can also create an event listener in your javascript, and if you feel adventurous, do the filtering via Ajax instead of reloading the page.

And one more thing:

The option value should be the category/status etc.

<option value="<?php echo $item->category->html() ?>"><?php echo $item->category()->html() ?></option>

You can also get the options for the selects by plucking the values from each field:

$categories = page('projects')->children()->pluck('category', null, true);
$status = page('projects')->children()->pluck('status', null, true);

Oh we’re so close. Got an error – found it. This gives me an empty selector for some reason and no project list.

<?php

// main menu items

$status = page('projects')->children()->pluck('status', null, true);

// only show the menu if items are available


?>
<form id="filters" action="" method="post">
<select onchange="this.form.submit()">
  <?php foreach($status as $item): ?>
  <option value="<?php echo $item->status->html() ?>"><?php echo $item->status()->html() ?></option>
  <?php endforeach ?>
</select>
</form>

You are trying to get a field from an array which does not work; now your status items are just the field values, so your select should look like this:

<form id="filters" action="" method="post">
  <select onchange="this.form.submit()">
    <?php foreach($status as $item): ?>
      <option value="<?php echo $item->html() ?>"><?php echo $item->html() ?></option>
    <?php endforeach ?>
  </select>
</form>

Thanks, this is starting to work. I now get my fields as dropdownmenus but unfortunately they won’t filter my list.
I will definitely try to make this work with ajax in the future to speed things up.

<?php

$status = page('projects')->children()->pluck('status', null, true);
$year = page('projects')->children()->pluck('year', null, true);

?>
<form id="filters" action="" method="post">
  <select onchange="this.form.submit()">
    <?php foreach($status as $item): ?>
      <option value="<?php echo $item->html() ?>"><?php echo $item->html() ?></option>
    <?php endforeach ?>
  </select>
   <select onchange="this.form.submit()">
    <?php foreach($year as $item): ?>
      <option value="<?php echo $item->html() ?>"><?php echo $item->html() ?></option>
    <?php endforeach ?>
  </select>
</form>




  <?php foreach($projects as $project): ?>
   <a href="<?php echo $project->url() ?>"> 
   
    <?php echo $project->id000()->html() ?>
  <?php echo $project->title()->html() ?> / 
  <?php echo $project->status()->html() ?>  / 
  <?php echo $project->year()->html() ?>  / 
  <?php echo $project->cat()->html() ?>
    </a></br>
  <?php endforeach ?>

Does the form get posted? You can test by putting this line somewhere into your template:

<?php var_dump($_POST) ?>

This is what I used a while back for a client project and it worked fine. Still required a submit button but worked still.

This was the main chunk of code in my controller:

    <?php
        $items = $page->children();
        $unfiltered = $items->paginate(10);
        $pagination = $unfiltered->pagination();
        $filter01 = $items->pluck('title', '', true);
        $filter02 = $items->pluck('reference', '', true);
        $filter03 = $items->pluck('categories', '', true);
        $filter04 = $items->pluck('online', '', true);
        $filter05 = $items->pluck('ready', '', true);

            if(r::is('post') and get('submit')) {
                $logic01 = (get('title') == "")          ? '!=' : '*=' ;
                $logic02 = (get('reference') == "")      ? '!=' : '*=' ;
                $logic03 = (get('categories') == "")     ? '!=' : '*=' ;
                $logic04 = (get('online') == "")         ? '!=' : '*=' ;
                $logic05 = (get('ready') == "")          ? '!=' : '*=' ;

                $filtered = '';
                $filtered = page('shop')->children()
                    ->filterBy('title', $logic01, get('title'))
                    ->filterBy('reference', $logic02, get('reference'))
                    ->filterBy('categories', $logic03, get('categories'))
                    ->filterBy('online', $logic04, get('online'))
                    ->filterBy('ready', $logic05, get('ready'))
                    ->paginate(10); 
                $pagination = $filtered->pagination();
            }      

This was my template code:

    <form method="get">
        
            <label for="title">Select Title</label>
            <select name="title">
                <option value="">All</option>
                <?php foreach($filter01 as $f): ?>
                    <option value="<?php echo $f->name()?>"
                        <?php if(r::is('get') and (strval($f->name()) == get('title'))) echo ' selected ="selected" ';?>> 
                        <?php echo $f->name() ?>
                    </option>
                <?php endforeach ?>
            </select>

            <label for="reference">Select Reference</label>
            <select name="reference">
                <option value="">All</option>
                <?php foreach($filter02 as $f): ?>
                    <option value="<?php echo $f->name()?>"
                        <?php if(r::is('get') and (strval($f->name()) == get('reference'))) echo ' selected ="selected" ';?>> 
                        <?php echo $f->name() ?>
                    </option>
                <?php endforeach ?>
            </select>

            <label for="categories">Select Categories</label>
            <select name="categories">
                <option value="">All</option>
                <?php foreach($filter03 as $f): ?>
                    <option value="<?php echo $f->name()?>"
                        <?php if(r::is('get') and (strval($f->name()) == get('categories'))) echo ' selected ="selected" ';?>> 
                        <?php echo $f->name() ?>
                    </option>
                <?php endforeach ?>
            </select>

            <label for="online">Product Online</label>
            <select name="online">
                <option value="">All</option>
                <?php foreach($filter04 as $f): ?>
                    <option value="<?php echo $f->name()?>"
                        <?php if(r::is('get') and (strval($f->name()) == get('online'))) echo ' selected ="selected" ';?>> 
                        <?php echo $f->name() ?>
                    </option>
                <?php endforeach ?>
            </select>

            <label for="ready">Ready to Sell</label>
            <select name="ready">
                <option value="">All</option>
                <?php foreach($filter05 as $f): ?>
                    <option value="<?php echo $f->name()?>"
                        <?php if(r::is('get') and (strval($f->name()) == get('ready'))) echo ' selected ="selected" ';?>> 
                        <?php echo $f->name() ?>
                    </option>
                <?php endforeach ?>
            </select>

        <button type="submit" name="submit" value="Submit">Filter</button>

    </form>  
    

            <?php if($filtered): ?>
                <?php foreach($filtered as $f): ?>
                <td><?php echo thumb($f->images()->first(), array('width' => 150, 'crop' => true, 'quality' => 80)); ?></td>
                <td><?php echo $f->title() ?></td>
                <td><?php echo $f->reference() ?></td>
                <td><?php echo $f->categories() ?></td>
                <td><?php echo $f->online() ?></td>
                <td><?php echo $f->ready() ?></td>
                <?php endforeach ?>
            <?php else: ?>
                <?php foreach($unfiltered as $u): ?>
                <td><?php echo thumb($u->images()->first(), array('width' => 150, 'crop' => true, 'quality' => 80)); ?></td>
                <td><?php echo $u->title() ?></td>
                <td><?php echo $u->reference() ?></td>
                <td><?php echo $u->categories() ?></td>
                <td><?php echo $u->online() ?></td>
                <td><?php echo $u->ready() ?></td>
                <?php endforeach ?>
            <?php endif ?>

Maybe not the best way of going about things but all the filters worked fine :slight_smile:

I don’t think the form gets posted: array(0) { }

Thanks a lot! I’ll have a look at it :slight_smile:

Hm, I have it working in a test project; if you like, you can send me your project as a zip file and I’ll take a look.

Edit: You can also try to just leave it as it is now and add a submit button instead to see if it works then.

Hi, thanks so much! How can I send you a ZIP file? The uploader doesn’t allow anything but images…

You can send us an email with the file. In that case to sonja@getkirby.com.