As part of my recent struggle building a list of 5 recent blog posts, I decided that I should just write a custom Handlebars Helper that did what I wanted. After I wrote the helper, I figured out that there was actually a built-in way to accomplish the task, such that my custom code became an unnecessary and possibly buggy addition. I was very disappointed.
But I felt obligated to flesh out my #withPages helper and share it. It is based on the built-in #withSort helper code, with some basic lodash action on the pages array, and a light topping of error messages. But it could be handy.
A typical usage would be something like the following:
<h2>Recent Posts by Posted Date</h2>
<ul>
{{#withPages collection="posts" inflection="post" value="all" sortBy="data.posted" dir="desc" limit=5}}
<li>{{formatDate data.posted "%F"}}: Post - {{data.title}} ({{basename}})</li>
{{/withPages}}
</ul>
the arguments allow for filtering collections, sorting, and limiting the pages:
Collection Filtering
Filtering pages in a collection requires three arguments. By default, {{#withPages}} uses all pages.
collection
- the name of the collection defined in the Gruntfile (typically "tags", "posts", "keywords", etc.)inflection
- the inflection for the collection, defined in the Gruntfile ("tag", "post", "keyword", etc.)value
- the collection value to filter on, defined in the YAML font-matter of pages ("new", "all", "javascript", etc.)
Sorting
There are two arguments for sorting attribute and direction:
sortBy
- (optional) the page property to sort by, typically defined in the YAML front-matter of pages and prefixed with "data.", such as "data.posted". Default sorting is the collection default.dir
- (optional) Sort direction, one of "ascending", "asc", "descending", "desc". Ascending by default.
Limiting
You can limit the number of pages selected as follows:
limit
- (optional) the maximum number of pages to return
Diagnostics
There are two arguments to help troubleshoot selecting pages:
verbose
- (optional) true or false if you want verbose console logging. Defaults to false.name
- (optional) provide a unique name to this {{#withPages}} tag to help match verbose messages to markup. Defaults to a combination of the arguments.
Installation
For now, copy the code below into the file in your helpers folder. You do have a helpers folder, right? I might make this a plugin in the future.
(function() {
"use strict";
var _ = require("lodash");
function getPropertyFromSpec(obj, propertySpec) {
var properties = propertySpec.split('.');
var resultObj = obj;
_.each(properties, function (property) {
if (resultObj && resultObj[property]) {
resultObj = resultObj[property];
} else {
resultObj = null;
}
});
return resultObj;
}
module.exports.register = function(Handlebars, options) {
Handlebars.registerHelper("withPages", function(options) {
var result = "";
var verbose = options.hash.verbose;
var collectionName = options.hash.collection;
var inflection = options.hash.inflection;
var inflectionValue = options.hash.value;
var limit = options.hash.limit;
var sortBy = options.hash.sortBy;
var sortDir = options.hash.dir;
var sortDescending = options.hash.dir && (sortDir === "desc" || sortDir === "descending");
var name = options.hash.name || "" + [collectionName, inflection, inflectionValue, sortBy, sortDir, limit].join("/");
var sourcePageName = this.page.src.substr(this.page.src.lastIndexOf("/") + 1);
var logPrefix = "{{#withPages}} " + name + " in " + sourcePageName + ": ";
function log(level, message, extra) {
if (verbose || level !== "info") {
if (extra) {
console[level](logPrefix + message, extra);
} else {
console[level](logPrefix + message);
}
}
}
log("info", "{{withPages}} options:", options.hash);
if (sortDir && (sortDir !== "asc" && sortDir !== "ascending" && sortDir !== "desc" && sortDir !== "descending")) {
log("warn", "{{withPages dir parameter '" + sortDir + "' is not recognized. " +
"Use one of 'asc', 'ascending', 'desc', 'descending'");
}
var selectedPages = this.pages;
// Collection filtering
if (collectionName && inflection && inflectionValue) {
var collection = this[collectionName];
if (!collection) {
log("error", "collection '" + collectionName + "' not found");
return result;
}
if (collection.length > 0 && !collection[0][inflection]) {
log("warn", "{{withPages}} did not find pages matching inflection '" + inflection +
"' in collection '" + collectionName + "'");
return result;
}
var collectionFilter = {};
collectionFilter[inflection] = inflectionValue;
var collectionSubset = _.find(collection, collectionFilter);
if (collectionSubset && collectionSubset.pages && collectionSubset.pages.length > 0) {
log("info", "{{withPages}} found " + collectionSubset.pages.length + " pages in collection '" +
collectionName + "' with inflection '" + inflection + "' and value '" + inflectionValue + "'");
selectedPages = collectionSubset.pages;
} else {
log("info", "{{withPages}} did not find any pages in collection '" + collectionName +
"' with inflection '" + inflection + "' and value '" + inflectionValue + "'");
return result;
}
} else {
log("info", "using all " + selectedPages.length + " pages");
}
// Sorting
if (sortBy) {
var pagesWithSortByValue = 0;
selectedPages = _.sortBy(selectedPages, function (item) {
var sortByValue = getPropertyFromSpec(item, sortBy);
if (sortByValue) {
pagesWithSortByValue++;
}
return sortByValue;
});
log("info", "{{withPages}} sorted by '" + sortBy + "'. " + pagesWithSortByValue + " of " +
selectedPages.length + " pages had '" + sortBy + "' values.");
if (pagesWithSortByValue < selectedPages.length && sortBy.indexOf("data.") == -1) {
log("info", "{{withPages}} Did you mean to use sortBy 'data." + sortBy + "'?");
}
}
if (sortDescending) {
selectedPages = selectedPages.reverse();
log("info", "{{withPages}} reversing sort order");
}
// Limit
if (limit && limit >= 0) {
var limitedPages = _.first(selectedPages, limit);
log("info", "{{withPages}} limiting to " + limitedPages.length + " pages out of " +
selectedPages.length);
selectedPages = limitedPages;
}
// Rendering
_.each(selectedPages, function (page, index, pages) {
log("info", "{{withPages}} rendering page: " + page.basename);
result += options.fn(page);
});
return result;
});
};
}).call(this);
For the code, please see the learning-assemble repository on GitHub. The branch step12-navigation-helper contains the completed code from this post.