64

I have a Handlebars template where I'm trying to generate a comma-separated list of items from an array.

In my Handlebars template:

{{#each list}}
    {{name}} {{status}},
{{/each}}

I want the , to not show up on the last item. Is there a way to do this in Handlebars or do I need to fall back to CSS selectors?

UPDATE: Based on Christopher's suggestion, this is what I ended up implementing:

var attachments = Ember.CollectionView.extend({
    content: [],
    itemViewClass: Ember.View.extend({
        templateName: 'attachments',
        tagName: 'span',
        isLastItem: function() {
            return this.getPath('parentView.content.lastObject') == this.get('content');
        }.property('parentView.content.lastObject').cacheable()
    })
}));

and in my view:

{{collection attachments}}

and the item view:

{{content.title}} ({{content.size}}) {{#unless isLastItem}}, {{/unless}}
Chris Thompson
  • 14,633
  • 9
  • 40
  • 57
  • Another thing that I found out is that if you remove the last item, you need to force a redraw of each item to remove the last separator. By default removing an item seems to only remove the view for that item and the others are not updated (which makes sense). – Chris Thompson May 09 '12 at 15:54

8 Answers8

148

I know I'm late to the parts but I found a WAYYYY simpler method

{{#unless @last}},{{/unless}}
MylesBorins
  • 1,734
  • 2
  • 10
  • 9
59

Since Ember v1.11 you are able to get the index of an each using block parameters. In your case this would look something like this:

{{#each list as |item index|}}
    {{if index ", "}}{{item.name}} {{item.status}}
{{/each}}

The first index value will be 0 which will evaluate to false and will not be added, all subsequent values will evaluate to true which will prepend a separator.

Jon Koops
  • 6,949
  • 5
  • 24
  • 45
Nahkala
  • 601
  • 5
  • 4
50

You can use standard CSS to do this:

li:after {
    content: ',';
}

li:last-of-type:after {
    content: '';
}

I prefer separate rules, but a more concise if slightly less readable version (from @Jay in the comments):

li:not(:last-of-type):after {
    content: ',';
}
SE_net4 the downvoter
  • 21,043
  • 11
  • 69
  • 107
Christopher Swasey
  • 9,842
  • 1
  • 29
  • 23
  • I looked into last-child and though it would work well except for the fact that it's not supported in IE8. I'll give the computed property a shot. It still seems odd to me that there's no built-in way to make a delimited list. – Chris Thompson May 02 '12 at 06:12
  • 5
    Rather than using `:after` and `last-child`, you can use `li + li:before`, making this solidly IE8-proof (don't forget to [put it in standards compliant rendering mode](http://zurb.com/article/284/ie8-rendering-modes-one-meta-tag-to-rule-)!) – Erwin Wessels Jun 14 '13 at 07:08
  • This is perfect! I'm using this with Mustache.js templates, but I didn't find it until I started looking around for a replacement (which I now don't need!). – Harry Pehkonen Mar 12 '15 at 13:39
  • I've edited the answer below, maybe you should incorporate this new syntax into your answer as well. – Jon Koops May 14 '15 at 12:41
  • I've edited to remove old crufty fallbacks. If you're still caring about IE8... god have mercy on your soul. I still prefer the explicit separate rules, but to each his/her own. – Christopher Swasey Apr 02 '17 at 02:10
  • While this does solve the problem, it does not answer the actual question. – thomaux Jun 25 '19 at 10:14
5

I realize this is a year old but I had a similar problem and wound up here. In my case, I was actually dealing with an array. So, here's my solution.

Handlebars.registerHelper('csv', function(items, options) {
    return options.fn(items.join(', '));
});

// then your template would be
{{#csv list}}{{this}}{{/csv}}

I was going for a simple and elegant solution that keeps the csv logic in the template.

freak3dot
  • 113
  • 1
  • 6
5

I have created sep block helper:

Handlebars.registerHelper("sep", function(options){
    if(options.data.last) {
        return options.inverse();
    } else {
        return options.fn();
    }
});

Usage:

{{#each Data}}
   {{Text}}{{#sep}},{{/sep}}
{{/each}}

Supports else statement.

Jadro007
  • 115
  • 3
  • 9
4

With ember 2.7 you can do this after you install ember-truth-helpers:

ember install ember-truth-helpers

and then your template will look like this:

{{#each model as |e|}}
    {{e}}{{#unless (eq e model.lastObject)}}, {{/unless}}
{{/each}}
albertjan
  • 7,411
  • 5
  • 39
  • 70
3

I got this working with a modified version of freak3dot's answer:

handlebars.registerHelper('csv', function(items, options) {
  return items.map(function(item) {
    return options.fn(item)
  }).join(', ')
})

(This is a node app, so change the map accordingly to underscore or whatever if you're building in the browser)

Allows for formatting objects between each comma:

{{#csv players}
  {{firstName}} {{lastName}}
{{/csv}}

Edit: Here's a more flexible version. Join a list of things on an arbitrary separator.

handlebars.registerHelper('join', function(items, separator, options) {
  return items.map(function(item) {
    return options.fn(item)
  }).join(separator)
})

And template:

{{#join players ' vs '}
  {{firstName}} {{lastName}}
{{/join}}
mmacaulay
  • 2,913
  • 21
  • 27
2

Maybe for this context, you should be creating a view for the collection, not an iteration of views on the member items. In this case, a Handlebar iterator is overkill. In my example below, changes to the firstName or lastName on the Person objects will be bound to the list and update the view.

Template:

{{App.listController.csv}}

Javascript:

App = Ember.Application.create();

var Person = Ember.Object.extend({
    firstName: null,
    lastName: null
});

var bob = Person.create({
    firstName: "bob",
    lastName: "smith"
});

var ann = Person.create({
    firstName: "ann",
    lastName: "doe"
});

App.listController = Ember.Object.create({
    list: [bob, ann],
    csv: Ember.computed(function () {
        var arr = [];
        this.get('list').forEach(function (item, index, self) {
            arr.push(item.firstName + ' ' + item.lastName);
        })
        return arr.join(',');
        }).property('list.@each.firstName', 'list.@each.lastName')
});
// any changes to bob or ann will update the view
bob.set('firstName', 'tim');
// adding or removing from the array will update the view
App.listController.get('list').pushObject(Person.create(firstName: "Jack", lastName:"Dunn"});

Below is my original answer, that didn't work for this context.

You should be able to do this with a helper:

Handlebars.registerHelper('csv', function(items, options) {
  var out = "";
  for(var i=0, l=items.length; i<l; i++) {
    out += options.fn(items[i]);
    if (i < l - 1) {
        out += ',';
    }
    // might want to add a newline char or something
  } 
  return out;
});

// then your template would be
{{#csv list}} {{name}} {{status}} {{/each}}
hellslam
  • 1,470
  • 11
  • 17
  • Unfortunately this doesn't work as-is with the databinding in Ember.js. – Chris Thompson May 01 '12 at 22:16
  • I ended up doing something similar to this but using the Ember.js ContainerView which supports a Handlebars template per item, and then a computed property that says if the current item is the last item in the parent's list or not. See my updated answer. This looks like a good option if you don't need to use a fancy HTML template (like I'm doing). – Chris Thompson May 02 '12 at 20:07