Render array of objects together with index in Mustache.js and as Bootstrap rows or dropdown options with selected value

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!