Came across a GitHub issue Collection processing like @index for Mustache.js. It referenced the each()
function in another library, GRMustache.
Came up with the following and posted my solution back in the Mustache.js GitHub issue 🙂
Sample output:
<h4>Test 1: Loop over array with count & access to existing template vars/fns</h4> <div data-date="2021-12-03">item 1/4: ant (age: 20)</div> <div data-date="2021-12-03">item 2/4: bee (age: 15)</div> <div data-date="2021-12-03">item 3/4: cat (age: 10)</div> <div data-date="2021-12-03">item 4/4: dog (age: 5)</div> <h4>Test 2: Loop over array without using "item." prefix</h4> Index is even. <div>index 0: ant (age: 20)</div> Index is odd. <div>index 1: bee (age: 15)</div> Index is even. <div>index 2: cat (age: 10)</div> Index is odd. <div>index 3: dog (age: 5)</div> <h4>Test 3: Render rows in Bootstrap with 3 columns per row</h4> <div class="row"> <div class="col-4">ant</div> <div class="col-4">bee</div> <div class="col-4">cat</div> </div> <div class="row"> <div class="col-4">dog</div> </div> <h4>Test 4: Populate dropdown with selected value</h4> <select name="mydropdown"> <option value="">Select a value</option> <option value="12">ant</option> <option value="34">bee</option> <option value="56" selected>cat</option> <option value="78">dog</option> </select>
Sample usage:
let template = ` <h4>Test 1: Loop over array with count & access to existing template vars/fns</h4> {{#each}}{{#items}} <div data-date="{{date}}"> item {{indexPlusOne}}/{{items.length}}: {{item.name}} (age: {{item.age}}) </div> {{/items}}{{/each}} <h4>Test 2: Loop over array without using "item." prefix</h4> {{#each}}{{#items}} Index is {{#isIndexEven}}even{{/isIndexEven}}{{^isIndexEven}}odd{{/isIndexEven}}. {{#item}} <div>index {{index}}: {{name}} (age: {{age}})</div> {{/item}} {{/items}}{{/each}} <h4>Test 3: Render rows in Bootstrap with 3 columns per row</h4> <div class="row"> {{#each}}{{#items}} {{^isFirst}} {{#isIndexMod3}}</div><div class="row">{{/isIndexMod3}} {{/isFirst}} <div class="col-4">{{item.name}}</div> {{/items}}{{/each}} </div> <h4>Test 4: Populate dropdown with selected value</h4> <select name="mydropdown"> <option value="">Select a value</option> {{#each}}{{#items}} <option value="{{item.id}}" {{#compare}}{{selected_value}}|{{item.id}}|selected{{/compare}}> {{item.name}} </option> {{/items}}{{/each}} </select> `; // templateVars from sample code below let output = mustache.render(template, templateVars); console.log(output);
Sample code:
const mustache = require('mustache'); let templateVars = { date: '2021-12-03', selected_value: 56, items: [ { id: 12, name: 'ant', age: 20, }, { id: 34, name: 'bee', age: 15, }, { id: 56, name: 'cat', age: 10, }, { id: 78, name: 'dog', age: 5, }, ], each: function () { // Convert array of objects into a collection with additional info such as // item, index, indexPlusOne, isIndexMod3, etc. See // https://github.com/janl/mustache.js/issues/645#issuecomment-985169265 // for more details on usage and output. let templateVars = this; let newTemplateVars = null; return function (text, render) { if (null === newTemplateVars) { // parse once newTemplateVars = Object.assign({}, templateVars); let found = text.match(/^\{\{#([^\}]+)\}\}/i); if (found) { let variableName = found[1]; let variable = templateVars[variableName] || []; let lastIndex = variable.length - 1; newTemplateVars[variableName] = []; variable.forEach((item, index) => { newTemplateVars[variableName].push({ // Properties provided for each member in the collection // (from each object in the array) item: item, index: index, indexPlusOne: (index + 1), isIndexEven: (0 === index % 2), isFirst: (0 === index), isLast: (lastIndex === index), // For Bootstrap columns, use isIndexEven for isIndexMod2 isIndexMod3: (0 === index % 3), isIndexMod4: (0 === index % 4), isIndexMod6: (0 === index % 6), isIndexMod8: (0 === index % 8), isIndexMod12: (0 === index % 12), }); }); } } return mustache.render(text, newTemplateVars); }; }, compare: function () { // E.g.: {{#compare}}{{pet.type}}|cat|meow|{{pet.sound}}{{/compare}} yields // "meow" if the template variable `pet.type` has the value "cat", else it // will yield the template var `pet.sound`. The text passed to the function // is a pipe-delimited list with the format // "<variable name>|<value>|<output if true>|<output if false>", with the // <output if false> being optional. If <value> is omitted, // e.g. "{{pet.type}}||meow", the variable will be checked if it is empty. // All parts in the list can use Mustache tags. // E.g. for inverse condition: // {{#compare}}{{pet.type}}|!cat|<a href="#">{{pet.type}}</a>|meow{{/compare}} // yields <a href="#">dog</a> if the template variable `pet.type` has the // value "dog" and yields "meow" if the value is "cat". return function (text, render) { let parts = text.split('|').map((val) => val.trim()); let variable = render(parts?.[0] ?? ''); let value = parts?.[1] ?? ''; // no render() yet cos of NOT condition let isNotCondition = (0 === value.indexOf('!')); if (isNotCondition) { value = value.substr(1); } let renderedValue = render(value); let isTrue = ('' === renderedValue) ? utils.isEmpty(variable) : (renderedValue == variable); // == not === if (isNotCondition) { isTrue = !isTrue; } return render((isTrue ? parts?.[2] : parts?.[3]) ?? ''); }; }, };
Hope this is useful, ad huc!