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 0
or 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:
- How can you sort the
startdate
andstarttime
in one step? - 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!