Grouping items in a template
There was a question on Perch forum about grouping list items by the value of a field. Specifically they wanted to group items by the first letter of the item’s name. So the desired HTML would be something like this:
<section>
<h2>A</h2>
<ul>
<li>Alex...</li>
<li>Amelia...</li>
</ul>
<h2>B</h2>
<ul>
<li>Ben...</li>
<li>Ben...</li>
<li>Bryan...</li>
</ul>
<h2>C</h2>
<ul>
<li>Chris...</li>
</ul>
.
.
</section>
For context, let’s say we have a Collection or a multiple-item Region for a company’s employees:
<perch:content id="name" type="text" label="Name" title>
<perch:content id="role" type="text" label="Role">
perch_collection('Team');
You can sort items alphabetically with the sort
options. The issue is grouping the items alphabetically.
It is not practical to make 26 database queries (one for each letter). You would make a query to get a list of items which have name
values that start with a
, then another for those that start with b
, and so on.
Instead we can achieve this with a single perch_collection()
call and some Perch templating magic.
Sort
The first step is to sort the items by the field you want. We’ll use the name
field from our example:
perch_collection('Team', [
'sort' => 'name',
'sort-type' => 'alpha',
'sort-order' => 'ASC',
]);
This sorts the items by name
from A to Z.
Template
A basic list template:
<perch:before>
<h1>Our Team</h1>
<ul>
</perch:before>
<li>
<perch:content id="name" type="text">
</li>
<perch:after>
</ul>
</perch:after>
The above outputs all the item in a single list <ul>
:
<ul>
<li>Alexander</li>
<li>Amelia</li>
<li>Ben</li>
<li>Ben</li>
<li>Bryan</li>
<li>Chris</li>
</ul>
Now we have a number of challenges:
- Dynamically output the first letter of
name
- Conditionally perform (1) when
name
is the first occurrence that starts with this letter - Conditionally open a
<ul>
tag whenname
is the first occurrence that starts with this letter - Conditionally close
</ul>
tag whenname
is the last occurence that starts with this letter
So when we “start a new group”, we perform 1-3. When we “end a group” we perform 4.
Output the first letter
We can use the format attribute to limit the number of characters we output format="C:length"
:
<perch:content id="name" type="text" format="C:1">
This can also be achieved with the chars attribute:
<perch:content id="name" type="text" chars="1">
While the chars
attribute is perhaps easier to read, it is important to understand the syntax of the format
attribute as we’ll need use it later in some conditional tags.
Comparing items
We need to conditionally start and end groups (open/close <ul>
). In order to do this, we need to compare the current item we are rendering with the previous item.
Perch template engine renders what’s inside perch:before
first, then renders one item at a time and finally it renders what’s inside perch:after
. We want to perform the comparison in the phase in which it renders one item at a time.
This is where we use Perch conditional tags. perch:if allows us to compare a field value from the current item to the equivalent field from the previous item using the different
attribute. To check whether the value of the name
field of the current item is the same as the previous item’s:
<perch:if different="name">
Different
<perch:else>
Same
</perch:if>
Let’s add this to the basic list template we started with:
<perch:before>
<h1>Our Team</h1>
<ul>
</perch:before>
<li>
<perch:content id="name" type="text">
<perch:if different="name">
(Different from above)
<perch:else>
(Same as above)
</perch:if>
</li>
<perch:after>
</ul>
</perch:after>
The output would be:
<ul>
<li>Alexander (Different from above)</li>
<li>Amelia (Different from above)</li>
<li>Ben (Different from above)</li>
<li>Ben (Same as above)</li>
<li>Bryan (Different from above)</li>
<li>Chris (Different from above)</li>
</ul>
This is cool, but in our context we need to compare the first letter of the name
field value rather than the whole string. We can perform this comparison by using the format
and format-both
attributes on the perch:if
tag:
<perch:if different="name" format="C:1" format-both>
Different first letter
<perch:else>
Same first letter
</perch:if>
The format
attribute here behaves the same way as when used on a normal tag. The format-both
attribute specifies whether the formatting should be applied to both the current and previous items’ values.
We can also tell Perch to make the comparison case insensitive by using the case
attribute:
<perch:if different="name" format="C:1" format-both case="insensitive">
Different first letter
<perch:else>
Same first letter
</perch:if>
Applying this to our basic list template:
<perch:before>
<h1>Our Team</h1>
<ul>
</perch:before>
<li>
<perch:content id="name" type="text">
<perch:if different="name" format="C:1" format-both case="insensitive">
(Different first letter from above)
<perch:else>
(Same first letter as above)
</perch:if>
</li>
<perch:after>
</ul>
</perch:after>
The output would be:
<ul>
<li>Alexander (Different first letter from above)</li>
<li>Amelia (Same first letter as above)</li>
<li>Ben (Different first letter from above)</li>
<li>Ben (Same first letter as above)</li>
<li>Bryan (Same first letter as above)</li>
<li>Chris (Different first letter from above)</li>
</ul>
Conditionally start and close groups
We can use the same perch:if
tag to determine when to start a new group:
<perch:before>
<h1>Our Team</h1>
</perch:before>
<perch:if different="name" format="C:1" format-both case="insensitive">
<!--* Start a group *-->
<h2>
<perch:content id="name" type="text" format="C:1">
</h2>
<ul>
</perch:if>
<!--* Output the item *-->
<li>
<perch:content id="name" type="text">
</li>
Note how in the previous example, the first item evaluates to different. This means the above template will start a new group when rendering the first item too.
Everytime we start a new group it means we are done with the previous group. So we can close group inside the same conditional tags:
<perch:before>
<h1>Our Team</h1>
</perch:before>
<perch:if different="name" format="C:1" format-both case="insensitive">
<!--* close previous group *-->
</ul>
<!--* Start a group *-->
<h2>
<perch:content id="name" type="text" format="C:1">
</h2>
<ul>
</perch:if>
<!--* Output the item *-->
<li>
<perch:content id="name" type="text">
</li>
<perch:after>
</perch:after>
However, the above adds a closing tag </ul>
before starting the first group. Let’s add another conditional tag to ignore the first item:
<perch:before>
<h1>Our Team</h1>
</perch:before>
<perch:if different="name" format="C:1" format-both case="insensitive">
<!--* close previous group, ignore first item *-->
<perch:if not-exists="perch_item_first">
</ul>
</perch:if>
<!--* Start a group *-->
<h2>
<perch:content id="name" type="text" format="C:1">
</h2>
<ul>
</perch:if>
<!--* Output the item *-->
<li>
<perch:content id="name" type="text">
</li>
<perch:after>
</perch:after>
If it seems odd to you to add the closing tag </ul>
before the opening <ul>
and before the <li>
tags, remember the different
attribute is used to compare against the previous item.
This brings me to the last point. Because we only output the closing tag </ul>
when we are starting a new group, we never actually close the last group. That’s why we need to add another closing tag inside perch:after
:
<perch:before>
<h1>Our Team</h1>
</perch:before>
<perch:if different="name" format="C:1" format-both case="insensitive">
<!--* Close previous group, ignore first item *-->
<perch:if not-exists="perch_item_first">
</ul>
</perch:if>
<!--* Start a new group *-->
<h2>
<perch:content id="name" type="text" format="C:1">
</h2>
<ul>
</perch:if>
<!--* Output the item *-->
<li>
<perch:content id="name" type="text">
</li>
<perch:after>
<!--* Close the last group *-->
</ul>
</perch:after>