77
votes

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?

6
I would recommend underscoreJS's template library, It makes much more sense and is much more efficient than handlebars.js - Austin
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
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
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
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

6 Answers

115
votes

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}}
160
votes

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}}
26
votes

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.

1
votes

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.

0
votes

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}}
0
votes

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.