In Mustache, How to get the index of the current Section

TemplatesMustache

Templates Problem Overview


I am using Mustache and using the data

{ "names": [ {"name":"John"}, {"name":"Mary"} ] }

My mustache template is:

{{#names}}
    {{name}}
{{/names}}

What I want to be able to do is to get an index of the current number in the array. Something like:

{{#names}}
    {{name}} is {{index}}
{{/names}}

and have it print out

John is 1
Mary is 2

Is it possible to get this with Mustache? or with Handlebars or another extension?

Templates Solutions


Solution 1 - Templates

This is how I do it in JavaScript:

var idx = 0;

var data = { 
   "names": [ 
       {"name":"John"}, 
       {"name":"Mary"} 
    ],
    "idx": function() {
        return idx++;
    }
};

var html = Mustache.render(template, data);

Your template:

{{#names}}
    {{name}} is {{idx}}
{{/names}}

Solution 2 - Templates

For reference, this functionality is now built in to Handlebars which has compatibility with Mustache.

Use {{@index}}

{{#names}}
    {{name}} is {{@index}}
{{/names}}

John is 0

Mary is 1

Solution 3 - Templates

In handlebars.js you can accomplish this with a helper function. (In fact, one of the advantages mentioned about handlebars here http://yehudakatz.com/2010/09/09/announcing-handlebars-js/ is that you can use helpers instead of having to rewrite the objects before calling the template.

So, you could do this:

  var nameIndex = 0;
  Handlebars.registerHelper('name_with_index', function() {
    nameIndex++;
    return this.name + " is " + nameIndex;
  })

And, then your template can be this:

{{#names}}
<li>{{name_with_index}}</li>
{{/names}}

Your data is the same as before, i.e.:

{ "names": [ {"name":"John"}, {"name":"Mary"} ] };

And you get this output:

<li>John is 1</li>
<li>Mary is 2</li>

To make this really work, nameIndex needs to get reset each time the template is rendered, so to do that you can have a reset helper at the beginning of the list. So full code looks like this:

  var data = { "names": [ {"name":"John"}, {"name":"Mary"} ] };
  var templateSource = "<ul>{{reset_index}}{{#names}}<li>{{name_with_index}}</li>{{/names}}</ul>";
  var template = Handlebars.compile(templateSource);

  var helpers = function() {
    var nameIndex = 0;
    Handlebars.registerHelper('name_with_index', function() {
      nameIndex++;
      return this.name + " is " + nameIndex;
    });
    Handlebars.registerHelper('reset_index', function() {
      nameIndex = 0;
    })
  }();

  var htmlResult= template(data);
  $('#target').html(htmlResult);

  var htmlResult2= template(data);
  $('#target2').html(htmlResult2);

(This can correctly render the template twice.)

Solution 4 - Templates

You can run the following loop on your list of objects.

This solution has the following advantages:

  • Does not change the model data
  • The index can be accessed multiple times

Code:

for( var i=0; i< the_list.length; i++) {
    the_list[i].idx = (function(in_i){return in_i+1;})(i);
}

Explanation:

A function is used instead of a variable name, so the data is not mutated. Closure is used to return the value of 'i' at the time the function is created in the loop instead of the value of i at the end of the loop.

Solution 5 - Templates

A better approach for Mustache would be using a function that gets the index using indexOf:

var data = { 
   names: [ {"name":"John"}, {"name":"Mary"} ],
    index: function() {
        return data.names.indexOf(this);
    }
};

var html = Mustache.render(template, data);

In your template:

{{#names}}
    {{name}} is {{index}}
{{/names}}

The drawback is that this index function has the array hard coded data.names to make it a more dynamic, we can use another function like this:

var data = { 
   names: [ {"name":"John"}, {"name":"Mary"} ],
    index: function() {
        return function(array, render) {
            return data[array].indexOf(this);
        }
    }
};

var html = Mustache.render(template, data);

Usage in your template:

{{#names}}
    {{name}} is {{#index}}names{{/index}}
{{/names}}

Also a small drawback is that you have to pass the array name to the index function in this example names, {{#index}}names{{/index}}.

Solution 6 - Templates

Great helper here:

// {{#each_with_index records}}
// 	<li class="legend_item{{index}}"><span></span>{{Name}}</li>
// {{/each_with_index}}

Handlebars.registerHelper("each_with_index", function(array, fn) {
	var buffer = "";
	for (var i = 0, j = array.length; i < j; i++) {
		var item = array[i];

		// stick an index property onto the item, starting with 1, may make configurable later
		item.index = i+1;

		// show the inside of the block
		buffer += fn(item);
	}

	// return the finished buffer
	return buffer;

});

Source: https://gist.github.com/1048968

Solution 7 - Templates



My recommendation is to add an index key

let data = {
  "names": [
    {"name": "John"}, 
    {"name": "Mary"}
  ]
};

// Add an index manually
data = {
  names: [
    { id: 0, name: "John" },
    { id: 1, name: "Mary" }
  ]
};

// Add an index with a loop
data = data.names.map(function (name, i) {
  name.id = i;
  return name;
});

// Nested arrays with indices (parentId, childId)
data = {
  names: [{
    parentId: 0,
    name: "John",
    friends: [{
      childId: 0,
      name: "Mary"
    }]
  }]
};

Problems with proposed Mustache Index solutions


Other proposed solutions are not as clean, as detailed below...

@Index

Mustache does not support @Index

IndexOf

This solution runs in O(n^2) time, and therefore it does not have the best performance. Also the index must be manually created for each list.

const data = {
  list: ['a', 'b', 'c'],
  listIndex: function () {
    return data.list.indexOf(this);
  }
};

Mustache.render('{{#list}}{{listIndex}}{{/list}}', data);

Global Variable

This solution runs in O(n) time, so better performance here, but also "pollutes the global space" (i.e. adds properties to the window object, and the memory is not freed by the garbage collector until the browser window closes), and the index must be manually created for each list.

const data = {
  list: ['a', 'b', 'c'],
  listIndex: function () {
    return (++window.listIndex || (window.listIndex = 0));
  }
};

Mustache.render('{{#list}}{{listIndex}}{{/list}}', data);

Local Variable

This solution runs in O(n) time, does not "pollute the global space", and does not need to be manually created for each list. However, the solution is complex and does not work well with nested lists.

const data = {
  listA: ['a', 'b', 'c'],
  index: () => name => (++data[`${name}Index`] || (data[`${name}Index`] = 0))
};

Mustache.render('{{#listA}}{{#index}}listA{{/index}}{{/listA}}', data);

Solution 8 - Templates

If you can control the output of the JSON string, then try this out.

{ "names": [ {"name":"John", "index":"1"}, {"name":"Mary", "index":"2"} ] }

So when you make your JSON string, add the index as another property for each object.

Solution 9 - Templates

As long as you're not traveling into more than one array you can store the index on the helper and just increment when the context changes. I'm using something similar to the following:

var data = {
    foo: [{a: 'a', 'b': 'a'}, {'a':'b', 'b':'b'}], 
    i: function indexer() {
        indexer.i = indexer.i || 0;
        if (indexer.last !== this && indexer.last) {                                          
            indexer.i++;
        }   
        indexer.last = this;
        return String(indexer.i);
     }   
};

Mustache.render('{{#foo}}{{a}}{{i}}{{b}}{{i}}{{/foo}}', data);

Solution 10 - Templates

(Tested in node 4.4.7, moustache 2.2.1.)

If you want a nice clean functional way to do it, that doesn't involve global variables or mutating the objects themselves, use this function;

var withIds = function(list, propertyName, firstIndex) {
	firstIndex |= 0;
	return list.map( (item, idx) => {
		var augmented = Object.create(item);
		augmented[propertyName] = idx + firstIndex;
		return augmented;
	})
};

Use it when you're assembling your view;

var view = {
    peopleWithIds: withIds(people, 'id', 1) // add 'id' property to all people, starting at index 1
};

The neat thing about this approach is that it creates a new set of 'viewmodel' objects, using the old set as prototypes. You can read the person.id just as you would read person.firstName. However, this function doesn't change your people objects at all, so other code (which might have relied on the ID property not being there) will not be affected.

Algorithm is O(N), so nice and fast.

Solution 11 - Templates

My major use case for this was the ability to place an 'active' class on items. I messed around with combining an "equals" helper with and "index_for_each" helper, but it was way too complicated.

Instead, I came up with the following basic "active" helper. It's very specific, but is such a common use case for any menu / select list scenario:

Usage:

  <ul>
    {{{active 2}}}
    {{#each menuItem}}
      <li class="{{{active}}}">{{this}}</li>
    {{/each}}
  </div>

Would make the 3rd menu item have a "class='active'".

The helper is here (this is the CoffeeScript version):

active = 0
cur = 0
handlebars.registerHelper 'active', (item, context) ->
  if arguments.length == 2
    active = item
    cur = 0
    ''
  else if cur++ == active
    'active'

Solution 12 - Templates

I'm using a pre calculate function to inject '@index' into array.

const putIndexInArray = array => {
    let putIndex = (value, index) => {
        value['@index'] = index;

        return value;
    };

    if (!Array.isArray(array)) {
        return array;
    }

    return array.map(putIndex);
};

Mustache.render(template, {
    array: putIndexInArray(['Homer', 'Marge', 'Lisa'])
});

and then using like this:

{{#array}}
    {{@index}}
{{/array}}

Solution 13 - Templates

<script>var index = 1;</script>
{{#names}}
    {{name}} is <script>document.write(index++);</script>
{{/names}}

This works perfectly fine.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionchristophercottonView Question on Stackoverflow
Solution 1 - TemplatesdaveView Answer on Stackoverflow
Solution 2 - TemplatesKimView Answer on Stackoverflow
Solution 3 - TemplatesmattshView Answer on Stackoverflow
Solution 4 - TemplatesChris DutrowView Answer on Stackoverflow
Solution 5 - TemplatesPierreView Answer on Stackoverflow
Solution 6 - TemplateslukemhView Answer on Stackoverflow
Solution 7 - Templatestim-montagueView Answer on Stackoverflow
Solution 8 - TemplatessciritaiView Answer on Stackoverflow
Solution 9 - TemplatesfncompView Answer on Stackoverflow
Solution 10 - TemplatesSteve CooperView Answer on Stackoverflow
Solution 11 - TemplatesGuy BedfordView Answer on Stackoverflow
Solution 12 - TemplatesFernando ValView Answer on Stackoverflow
Solution 13 - TemplatesSagar DoluiView Answer on Stackoverflow