Creating an archive page with a custom Ghost helper to group a list of objects
TL;DR I have created a custom Handlebars helper for Ghost to group a list elements based on an arbitrary function. If you are only interested in using the helper, check out the Github repo and its wiki. The blog post below goes into a bit more detail on my motivation and the technical implementation.
Designing the feature
In one of my previous posts I described the new features that I have added to this blog. I also told you to stay tuned, because new features were coming :)
I had an archive page on the blog before, but it was simply just a list of posts, without any grouping. The problem is that there are no helpers in Handlebars or Ghost to group elements by a key. There is a custom helper available to group elements, but this can only do it based on a property of the objects. What I needed was to group posts objects based on the year and month component of their publication date, with a function like this:
function (item) {
var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
return new Date(item.published_at).getFullYear() + " " +monthNames[(new Date(item.published_at).getMonth())]; }
The component above can only be used for with a simple property and this is obviously more than that. I imagined a component that can be used like this:
{{#get 'posts' limit='all'}}
{{#group posts by='function (item) { var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; return monthNames[(new Date(item.published_at).getMonth())]+" "+(new Date(item.published_at)).getFullYear(); } '}}
<b>{{ key }} </b>
<br>
{{#each items}}
<span>
{{#if featured}}
<span class='fa fa-star'></span>
{{/if}}
{{date published_at format="MMM DD, YYYY"}}:
<a href="{{url}}">{{title}} </a>
</span>
<br>
{{/each}}
{{/group}}
{{/get}}
Basically, you specify a Javascript function as string in the by parameter and then it is used to calculate the grouping keys.
Implementing the helper
I used the code of original helper as a starting point. I modified the variables around a bit, but basically the only modification needed was to parse the function and use it as the basis for the grouping.
I found this little piece of code on Stackoverflow to parse the function:
function parseFunction(functionAsString) {
var funcReg = /function *\(([^()]*)\)[ \n\t]*{(.*)}/gmi;
var match = funcReg.exec(functionAsString.replace(/\n/g, ' '));
if(match) {
return new Function(match[1].split(','), match[2]);
}
return null;
};
Then comes the actual helper, as described in my previous post on helpers in general:
module.exports = function group(list, options) {
}
The first parameter will be the list to group, the second contains all the other parameters, in our case, the by parameter. Next comes the parameter validation and default value handling:
module.exports = function group(list, options) {
var options = options || {};
var inverse = options.inverse || noop;
var fn = options.fn || noop;
var groupingPredicate = parseFunction(options.hash.by);
var keys = [];
var groups = {};
// ...
if (!groupingPredicate || !list || !list.length) {
return inverse(this);
}
}
Then the function from the original helper is modified to calculate the keys based on the parsed predicate:
function addToGroup(item) {
var groupingKey = groupingPredicate(item);
if (keys.indexOf(groupingKey) === -1) {
keys.push(groupingKey);
}
if (!groups[groupingKey]) {
groups[groupingKey] = {
key: groupingKey,
items: []
};
}
groups[groupingKey].items.push(item);
}
The original function to render a group is left as it was:
function renderGroup(buffer, key) {
return buffer + fn(groups[key]);
}
And finally a we iterate through the list and place the elements in the groups and render all the groups:
list.forEach(addToGroup);
return keys.reduce(renderGroup, '');
And that's it! You now have a general purpose grouping helper. The only drawback is that currently, you cannot put linebreaks into the value of the by parameter (that's a limitation of the function parsing regex I found I think). I used this helper combined with the get helper in the above snippet to create my new Archive page.
Be sure to check out the full source of the helper at Github, and if you want to use it, you can find the instructions in the project wiki.