Open kimchristiansen opened 7 years ago
Came up with the following solution. Sample code, usage and output included. Also posted on my blog at https://blog.intzone.com/render-array-of-objects-together-with-index-in-mustache-js-and-as-bootstrap-rows-or-dropdown-options-with-selected-value/
Sample output
<h4>Test 1: Looping over array with count and access to existing template vars/functions</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: Looping 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: Rendering 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: Populating 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: Looping over array with count and access to existing template vars/functions</h4>
{{#each}}{{#items}}
<div data-date="{{date}}">item {{indexPlusOne}}/{{items.length}}: {{item.name}} (age: {{item.age}})</div>
{{/items}}{{/each}}
<h4>Test 2: Looping 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: Rendering 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: Populating 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 () {
// See https://github.com/janl/mustache.js/issues/645#issuecomment-985169265 which refers to
// https://github.com/groue/GRMustache/blob/master/Guides/standard_library.md#collection-processing
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({
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] ?? ''; // render() not used 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]) ?? '');
};
},
};
Is it possible to add collection processing like @index? Here is an example of an implementation: https://github.com/groue/GRMustache/blob/master/Guides/standard_library.md#collection-processing