conditional on last item in array using handlebars.js template
Asked Answered
H

6

89

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?

Hege answered 13/7, 2012 at 22:18 Comment(5)
I would recommend underscoreJS's template library, It makes much more sense and is much more efficient than handlebars.jsFouts
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.Subtotal
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.Hege
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"Pandowdy
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).Hege
S
128

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}}
Subhead answered 17/8, 2012 at 8:23 Comment(6)
Awesome job! Haven't verified that it works but I looks like it should, will revisit and reassign over weekend if so. Thanks!Hege
I have just used the above helper, worked exactly as advertised... thanks! gist.github.com/zeroasterisk/5360895Authority
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/5440241Torchier
Why is this answer accepted when the answer with first and last being native is more efficient?Decurion
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.Subhead
Sweet :) Thanks.Ferretti
G
174

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}}
Geryon answered 5/11, 2013 at 8:56 Comment(1)
{{#unless @last}},{{/unless}} FTW!Storiette
S
128

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}}
Subhead answered 17/8, 2012 at 8:23 Comment(6)
Awesome job! Haven't verified that it works but I looks like it should, will revisit and reassign over weekend if so. Thanks!Hege
I have just used the above helper, worked exactly as advertised... thanks! gist.github.com/zeroasterisk/5360895Authority
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/5440241Torchier
Why is this answer accepted when the answer with first and last being native is more efficient?Decurion
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.Subhead
Sweet :) Thanks.Ferretti
L
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.

Lofton answered 19/2, 2013 at 1:58 Comment(1)
Heads-up to anyone who found this and is looking to use it with Meteor's implementation of Handlebars, it won't work. The @ breaks everything.Pyre
H
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.

Hege answered 16/7, 2012 at 2:9 Comment(1)
This is answer is incomplete without a definition of compare and total. Also, 0 is the first index, not 1.Waterfall
W
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}}
Wring answered 2/10, 2013 at 16:57 Comment(0)
F
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.

Forespent answered 15/7, 2020 at 14:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.