janl / mustache.js

Minimal templating with {{mustaches}} in JavaScript
https://mustache.github.io
MIT License
16.43k stars 2.4k forks source link

Collection processing like @index #645

Open kimchristiansen opened 7 years ago

kimchristiansen commented 7 years ago

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

zionsg commented 2 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]) ?? '');
          };
      },
  };