88

I am leveraging handlebars.js for my templating engine and am looking to make a conditional segment display only if it is the last item in array contained in the templates configuration object.

{
  columns: [{<obj>},{<obj>},{<obj>},{<obj>},{<obj>}]
}

I've already pulled in a helper to do some equality/greater/less-than comparisons and have had success identifying the initial item this way but have had no luck accessing my target array's length.

Handlebars.registerHelper('compare', function(lvalue, rvalue, options) {...})

"{{#each_with_index columns}}"+
"<div class='{{#equal index 0}} first{{/equal}}{{#equal index ../columns.length()}} last{{/equal}}'>"+
"</div>"+
"{{/each_with_index}}"

Does anyone know a shortcut, different approach, and some handlebars goodness that will keep me from having to tear into the handlebars.js engine to determine best course?

techie.brandon
  • 1,638
  • 2
  • 18
  • 27
  • I would recommend underscoreJS's template library, It makes much more sense and is much more efficient than handlebars.js – Austin Jul 13 '12 at 22:21
  • 1
    Ummm, I <3 Underscore as much as the next guy, but you clearly have never looked seriously at Handlebars. Underscore's templating system can't (and will never even try to) do half the stuff Handlebars' can. – machineghost Jul 13 '12 at 22:25
  • 6
    I didn't ask about other templating engines. I have made my choice, many iterations ago. Many techs provide great solutions to this problem, but alas I am not working with those techs so they are useless to me right now. – techie.brandon Jul 13 '12 at 22:28
  • if you are talking simply "display", can you handle it with CSS? I realize it's a cop out, but if the data is already on the client, just hide the with `style="display: none"` – olore Jul 14 '12 at 02:28
  • Actually, using css won't work, as I am using this to signify which class to use. If first in the row (and found to be special) then it gets a special class, if last in the row (and found to be special) it gets a special class (some additional variables tell me what the special class is, so not standard to this table). – techie.brandon Sep 07 '12 at 14:16

6 Answers6

172

Since Handlebars 1.1.0, first and last has become native to the each helper. See ticket #483.

The usage is like Eberanov's helper class:

{{#each foo}}
    <div class='{{#if @first}}first{{/if}}{{#if @last}} last{{/if}}'>{{@key}} - {{@index}}</div>
{{/each}}
Community
  • 1
  • 1
Jacob van Lingen
  • 8,989
  • 7
  • 48
  • 78
126

As of Handlebars v1.1.0, you can now use the @first and @last booleans in the each helper for this problem:

{{#each foo}}
    <div class='{{#if @first}}first{{/if}}
                {{#if @last}} last{{/if}}'>
      {{@key}} - {{@index}}
    </div>
{{/each}}

A quick helper I wrote to do the trick is:

Handlebars.registerHelper("foreach",function(arr,options) {
    if(options.inverse && !arr.length)
        return options.inverse(this);

    return arr.map(function(item,index) {
        item.$index = index;
        item.$first = index === 0;
        item.$last  = index === arr.length-1;
        return options.fn(item);
    }).join('');
});

Then you can write:

{{#foreach foo}}
    <div class='{{#if $first}} first{{/if}}{{#if $last}} last{{/if}}'></div>
{{/foreach}}
Devin Rhode
  • 23,026
  • 8
  • 58
  • 72
Kara Brightwell
  • 2,529
  • 1
  • 21
  • 29
  • Awesome job! Haven't verified that it works but I looks like it should, will revisit and reassign over weekend if so. Thanks! – techie.brandon Sep 07 '12 at 14:12
  • I have just used the above helper, worked exactly as advertised... thanks! https://gist.github.com/zeroasterisk/5360895 – zeroasterisk Apr 11 '13 at 05:08
  • 5
    Nice answer, but I did notice one hangup. If the item in your array is a string, or any other primitive for that matter, you won't be able to append properties to it. My workaround was to create a new string with the value of item, which is technically an object, and attach the properties to that, like in this gist: [https://gist.github.com/jordancooperman/5440241](https://gist.github.com/jordancooperman/5440241) – jordancooperman Apr 23 '13 at 02:05
  • 3
    Why is this answer accepted when [the answer with `first` and `last` being native](http://stackoverflow.com/a/19784986/1269037) is more efficient? – Dan Dascalescu Nov 11 '14 at 21:52
  • 1
    Inertia. This answer was written and accepted two years ago. The version of Handlebars with the native constructs was released just over a year ago. I'm guessing the OP has moved on. – Kara Brightwell Nov 12 '14 at 17:45
  • Sweet :) Thanks. – Valentine Shi Jun 13 '19 at 06:15
28

If you just try to handle the first item of the array, this may help

{{#each data-source}}{{#if @index}},{{/if}}"{{this}}"{{/each}}

@index is provided by the each helper and for the first item, it would be equal to zero and thus can be handled by the if helper.

Yong Qu
  • 381
  • 3
  • 4
  • 7
    Heads-up to anyone who found this and is looking to use it with [Meteor](http://meteor.com)'s implementation of Handlebars, it won't work. The @ breaks everything. – Mike Turley Aug 27 '13 at 21:15
1

Solution:

<div class='{{#compare index 1}} first{{/compare}}{{#compare index total}} last{{/compare}}'></div>

Leveraging helpers from the following blog and gist...

https://gist.github.com/2889952

http://doginthehat.com.au/2012/02/comparison-block-helper-for-handlebars-templates/

// {{#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 total = array.length;
  var buffer = "";

  //Better performance: http://jsperf.com/for-vs-foreach/2
  for (var i = 0, j = total; 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;
    item.total = total;
    // show the inside of the block
    buffer += fn(item);
  }

  // return the finished buffer
  return buffer;

});

Handlebars.registerHelper('compare', function(lvalue, rvalue, options) {

    if (arguments.length < 3)
        throw new Error("Handlerbars Helper 'compare' needs 2 parameters");

    operator = options.hash.operator || "==";

    var operators = {
        '==':       function(l,r) { return l == r; },
        '===':      function(l,r) { return l === r; },
        '!=':       function(l,r) { return l != r; },
        '<':        function(l,r) { return l < r; },
        '>':        function(l,r) { return l > r; },
        '<=':       function(l,r) { return l <= r; },
        '>=':       function(l,r) { return l >= r; },
        'typeof':   function(l,r) { return typeof l == r; }
    }

    if (!operators[operator])
        throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);

    var result = operators[operator](lvalue,rvalue);

    if( result ) {
        return options.fn(this);
    } else {
        return options.inverse(this);
    }

});

Notice the starting index is correctly 1.

Odrade
  • 7,409
  • 11
  • 42
  • 65
techie.brandon
  • 1,638
  • 2
  • 18
  • 27
0

I made a little improvements in helper from Matt Brennan, you can use this helper with Objects or Arrays, this solution required Underscore library:

Handlebars.registerHelper("foreach", function(context, options) {
  options = _.clone(options);
  options.data = _.extend({}, options.hash, options.data);

  if (options.inverse && !_.size(context)) {
    return options.inverse(this);
  }

  return _.map(context, function(item, index, list) {
    var intIndex = _.indexOf(_.values(list), item);

    options.data.key = index;
    options.data.index = intIndex;
    options.data.isFirst = intIndex === 0;
    options.data.isLast = intIndex === _.size(list) - 1;

    return options.fn(item, options);
  }).join('');
});

Usage:

{{#foreach foo}}
    <div class='{{#if @first}}first{{/if}}{{#if @last}} last{{/if}}'>{{@key}} - {{@index}}</div>
{{/foreach}}
Community
  • 1
  • 1
ebaranov
  • 575
  • 7
  • 17
0

Just FYI: If you are stuck with Handlebars < 1.1.0 (like me) you might wann try this workaround:

Define a property like isLast on the objects you are iterating and use it like

{{#each objectsInList}}"{{property}}": "{{value}}"{{#unless isLast}},{{/unless}}{{/each}}

to build a JSON object.

Ditti
  • 47
  • 7