Auto-sort and auto-filter an structured events list with a Kirby Hook and page model

This post is based on the discussion from Auto sort structure field entries in panel. Thanks to @tobiasweh, @lukasbestle, @texnixe and @firekayne !

#Use case
Imagine you want to make a page that contains a list of chronological events. The easiest and best way to make such a list is - in my opinion - the structure field with table style.

So your blueprint events.yml looks like this:

title: Events
deletable: false
fields:
  title:
    label: Title
    type:  text
  events:
    label: Events
    type: structure
    modalsize: large
    style: table
    fields:
      startdate:
        label: Start date
        type: date
        width: 1/4
        format: DD.MM.YYYY
        required: true
      starttime:
        label: Start time
        type: time
        width: 1/4
        default: 20:00
        interval: 30
        required: true
      enddate:
        label: End date
        type: date
        format: DD.MM.YYYY
        width: 1/4
      endtime:
        label: End time
        type: time
        width: 1/4
        interval: 30
      eventtitle:
        label: Event title
        type: text
        width: 1/2
        required: true

In some cases it can be that the events list isn’t sorted chronologically. And sometimes you just forgot to clean up the events list by deleting expired events. So the events structure field should sort and filter itself by a given value. In this case it should sort and filter by the startdate field of an event.

Problem

The structure field doesn’t know that it should filter and sort itself and so it doesn’t do that.

Solution

A good way to auto-sort and auto-filter the structure field in Kirby is to use a Kirby hook that sorts and filters the field when the user clicks the save button of the events page. For this use case, this is the solution:

// Sorting events structure field by startdate
kirby()->hook('panel.page.update', function($page) {
  if(isset($page->content()->data['events'])) {
    $events = $page->events()->yaml();
    // Filtering expired dates
    $events = array_filter($events, function($k) {
      $today = date("Y-m-d");
      return strcmp($k['startdate'], $today) >= 0;
    });
    // Sorting dates
    usort($events, function($a, $b) {
      return strcmp($a['startdate'], $b['startdate']);
    });
    // Save result
    try {
      $page->update(array(
        'events' => yaml::encode($events)
      ));
    }
    catch(Exception $e) {
      echo $e->getMessage();
    }
  }
});

Paste this code snippet into site/config/config.php and it should work.

Let’s have a deeper look:

kirby()->hook('panel.page.update', function($page) {
  ...
});

This adds the Kirby hook, described in the docs at getkirby.com. Kirby Hooks aren’t template specific and always use the currently displayed page as value (similar like the $page value in templates), so we have to check if the field is available at that moment:

kirby()->hook('panel.page.update', function($page) {
  if(isset($page->content()->data['events'])) {
    ...
  }
});

Now we can read and work with the events field by saving the content as a variable:

$events = $page->events()->yaml();

Filter

The type of the $events is now an array. That’s really handy because we can operate with it by using standard php array functions. First of all, we should filter the array because it doesn’t make sense to sort expired dates first ;-):

$events = array_filter($events, function($k) {
  $today = date("Y-m-d");
  return strcmp($k['startdate'], $today) >= 0;
});

We’re using the array_filter(array $array, callable $filter_func) function to filter the events list and to build a new, filtered array. Our filter function returns a boolean. If the return is true, the function will write the value into the new array. To compare a date with an other date, we have to use the strcmp(string $a, string $b) function for that. This function returns an integer value but we have to return a boolean value for the array_sort function to filter the array.

An expired date will always be a “smaller” string in comparison with the today’s date. But we don’t want to delete an event that happens today - what results to 0 with a strcmp() comparison. So we want to filter every single event that’s 0or bigger than 0 in comparison. That’s why we have to return the value of strcmp($k['startdate'], $today) >= 0;.

Sort

To sort the events list, we simply use the suggestion by @lukasbestle using the usort(array $array, callable $compare_func):

usort($events, function($a, $b) {
  return strcmp($a['startdate'], $b['startdate']);
});

Save the result

In the end we have to save the result back as a value. Because we’re manipulating data we have to make sure we also catching unknown exceptions:

try {
  $page->update(array(
    'events' => yaml::encode($events)
  ));
}
catch(Exception $e) {
  echo $e->getMessage();
}

Bonus: Page models

The Kirby Hook updates the events list if the user is on that page in the panel and hits the save button. But to make really sure the user don’t have to press the button every day, we should filter the events list also in our Frontend. For this I’m using a page model for the events.php template to override the $page->events()->toStructure() function call:

<?
class EventsPage extends Page {
  
  public function events() {
    return parent::events()->toStructure()->sortBy('startdate', 'asc', 'starttime', 'asc')->filter(function($event) {
      $today = date("Y-m-d");
      return strcmp($event->startdate()->value, $today) >= 0;
    });
  }
}

Further Improvements

Now it’s your turn. I have two questions for you:

  1. How can you sort the startdate and starttime in one step?
  2. How can you manipulate the events list automagically (for example if a user opens the page the frontend page)?

Let me/us know, what you think!

4 Likes