Ember: Getting the index in #each loops

Unfortunately Ember’s version of handlebars does not support the {{@index}} value in a loop. If you do a google search you’ll find many people asking how to get this, without much help. The most frequent solution is to use the itemViewClass, which creates a nested view object for each iteration, which means you have to use view.parentView to access that templates view. What about a solution that just works out of the box?

Here I have created a handlebar helper that will loop just like you do with #each and provides the following helper values automatically:

  • index: The zero-based index
  • index_1: The one-based index
  • first: True if this is the first item in the list
  • last: True if this is the last item in the list
  • even: True if it’s an even iteration (0, 2, 4, 6)
  • odd: True if it’s an odd iteration (1, 3, 5)

Simple Example

Using it is simple:

{{#eachIndexed color in model}}
  <li>
    {{index_1}} - {{color}}
  </li>
{{/eachIndexed}}

Zebra rows

To get alternating row colors, you can use the odd/even boolean value provided by this helper:

{{#eachIndexed color in model}}
  <li {{bind-attr class="even odd"}}>
    {{index_1}} - {{color}}
  </li>
{{/eachIndexed}}


View fullscreen

Defining the Helper

To add this helper to your project, put this code into your app:

/**
  A replacement for #each that provides an index value (and other helpful values) for each iteration.
  Unless using `foo in bar` format, the item at each iteration will be accessible via the `item` variable.

  Simple Example
  --------------
  ```
  {{#eachIndexed bar in foo}}
    {{index}} - {{bar}}
  {{/#eachIndexed}}
  ```

  Helpful iteration values
  ------------------------
    * index: The current iteration index (zero indexed)
    * index_1: The current iteration index (one indexed)
    * first: True if this is the first item in the list
    * last: True if this is the last item in the list
    * even: True if it's an even iteration (0, 2, 4, 6)
    * odd: True if it's an odd iteration (1, 3, 5)
*/
Ember.Handlebars.registerHelper('eachIndexed', function eachHelper(path, options) {
  var keywordName = 'item',
      fn;

  // Process arguments (either #earchIndexed bar, or #earchIndexed foo in bar)
  if (arguments.length === 4) {
    Ember.assert('If you pass more than one argument to the eachIndexed helper, it must be in the form #eachIndexed foo in bar', arguments[1] === 'in');
    Ember.assert(arguments[0] +' is a reserved word in #eachIndexed', $.inArray(arguments[0], ['index', 'index+1', 'even', 'odd']));
    keywordName = arguments[0];

    options = arguments[3];
    path = arguments[2];
    options.hash.keyword = keywordName;
    if (path === '') { path = 'this'; }
  }

  if (arguments.length === 1) {
    options = path;
    path = 'this';
  }

  // Wrap the callback function in our own that sets the index value
  fn = options.fn;
  function eachFn(){
    var keywords = arguments[1].data.keywords,
        view = arguments[1].data.view,
        index = view.contentIndex,
        list = view._parentView.get('content') || [],
        len = list.length;

    // Set indexes
    keywords['index'] = index;
    keywords['index_1'] = index + 1;
    keywords['first'] = (index === 0);
    keywords['last'] = (index + 1 === len);
    keywords['even'] = (index % 2 === 0);
    keywords['odd'] = !keywords['even'];
    arguments[1].data.keywords = keywords;

    return fn.apply(this, arguments);
  }
  options.fn = eachFn;

  // Render
  options.hash.dataSourceBinding = path;
  if (options.data.insideGroup && !options.hash.groupedRows && !options.hash.itemViewClass) {
    new Ember.Handlebars.GroupedEach(this, path, options).render();
  } else {
    return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options);
  }
});