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 code:

  const mustache = require('mustache');

  let templateVars = {
      date: '2021-12-03',
      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 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 = {
                      parent: 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);
          };
      },
  };

  let template = `
      <h4>Test 1: Looping over array with count and access to parent scope</h4>
      {{#each}}{{#items}}
        <div data-date="{{parent.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" data-selected-value="56">
        <option value="">Select a value</option>
        {{#each}}{{#items}}
          <option value="{{item.id}}">{{item.name}}</option>
        {{/items}}{{/each}}
      </select>
      <script>
        let dropdown = document.querySelector('[name="mydropdown"]');
        let selectedValue = dropdown.getAttribute('data-selected-value');
        let option = dropdown.querySelector('option[value="' + selectedValue + '"]');
        if (option) {
            option.setAttribute('selected', '');
        }
      </script>
  `;

  let output = mustache.render(template, templateVars);
  console.log(output);

Sample output:

  <h4>Test 1: Looping over array with count and access to parent scope</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" data-selected-value="56">
    <option value="">Select a value</option>
    <option value="12">ant</option>
    <option value="34">bee</option>
    <option value="56">cat</option>
    <option value="78">dog</option>
  </select>
  <script>
    let dropdown = document.querySelector('[name="mydropdown"]');
    let selectedValue = dropdown.getAttribute('data-selected-value');
    let option = dropdown.querySelector('option[value="' + selectedValue + '"]');
    if (option) {
        option.setAttribute('selected', '');
    }
  </script>

Hope this is useful, ad huc!