If I have something like :
{
people: [
"Yehuda Katz",
"Alan Johnson",
"Charles Jolley"
]
}
How do I get the position of each of the values in handlebar?
{{#each people}}
{{this}}
<!--{{this.position}} -->
{{/each}}
I expect the answer to be
Yehuda Katz
0
Alan Johnson
1
Charles Jolley
2
You can define your own block expression, like 'each_with_index', to accomplish this. You can find out how to create this custom expression in the Handlebars documentation here: http://handlebarsjs.com/#block-expressions
Here's a quick and dirty way to do it:
var people = [ 'Yehuda Katz', 'Alan Johnson', 'Charles Jolley' ];
var source = '{{#each_with_index people}}' +
'{{ this.index }}: {{ this.item }}\n' +
'{{/each_with_index}}';
Handlebars.registerHelper('each_with_index', function(items, options) {
var output = '';
var item_count = items.length;
var item;
for (var index = 0; index < item_count; index ++) {
item = items[index];
output = output + options.fn({ item: item, index: index });
}
return output;
});
var $output = $('#output');
var template = Handlebars.compile(source);
var result = template({ people: people });
$output.html(result);
To see it working: http://jsfiddle.net/EECFR/1/
Use #index:
{{#each people}}
{{this}}
{{#index}}
{{/each}}
JSFiddle demo (modified from Carl's)
From http://handlebarsjs.com/#iteration:
When looping through items in each, you can optionally reference the current loop index via {{#index}}
{{#each array}}
{{#index}}: {{this}}
{{/each}}
Related
I'm returning a 3rd party object to handlebars but i've noticed that some property names are prefixed with unique namespaces. Here's a short extract:
var data={
"soapenv:Envelope":{
"soapenv:Body":{
"CaseDetails":[
{
"Status":"Open",
"Opened":"2018-02-19T10:56:03.783Z",
"ns1:CaseReference": {"id":"111111"}
},
{
"Status":"Closed",
"Opened":"2017-02-19T10:56:03.783Z",
"ns3:CaseReference": {"id":"222222"}
},
{
"Status":"Closed",
"Opened":"2016-02-19T10:56:03.783Z",
"ns8:CaseReference": {"id":"3333"}
}
]
}
}
};
I want to loop through this object and output the information. Is it possible to match these unique names: ns1:CaseReference, ns3:CaseReference and ns8:CaseReference?
{{#each data.soapenv:Envelope.soapenv:Body.CaseDetails}}
<td>{{Status}}</td>
<td>{{Opened}}</td>
<td>{{???.id}}</td>
{{/each}}
You can write a custom HandlebarsHelper, ifKeyHasText to check whether the key contains a specific string (CaseReference in this scenario).
Handlebars.registerHelper('ifKeyHasText', function(array, value, options) {
var key;
if(Object.keys(array).some(function(k){
if(~k.indexOf(value)){
key = k;
}
}));
if(key){
return options.fn(this[key]);
}
});
And in the template, you can pass the string CaseReference to the helper {{#ifKeyHasText}} along with the data JSON as this object.
{{#each soapenv:Envelope.soapenv:Body.CaseDetails}}
<td>{{Status}}</td>
<td>{{Opened}}</td>
<td>
{{#ifKeyHasText this 'CaseReference'}}
{{id}}
{{/ifKeyHasText}}
</td>
{{/each}}
Hope this helps.
I managed to get it based on this post: https://stackoverflow.com/a/21452230/1622376
var data={
"soapenv:Envelope":{
"soapenv:Body":{
"CaseDetails":[
{
"Status":"Open",
"Opened":"2018-02-19T10:56:03.783Z",
"ns1:CaseReference": {"id":"111111"}
},
{
"Status":"Closed",
"Opened":"2017-02-19T10:56:03.783Z",
"ns3:CaseReference": {"id":"222222"}
},
{
"Status":"Closed",
"Opened":"2016-02-19T10:56:03.783Z",
"ns8:CaseReference": {"id":"3333"}
}
]
}
}
};
Handlebar using 'this' to traverse the tree:
<ul>
{{#each soapenv:Envelope}}
{{#each this}}
{{#each this}}
<li>
{{this.Status}}
{{#each this}}
{{this.id}}
{{/each}}
{{this.Opened}}
</li>
{{/each}}
{{/each}}
{{/each}}
</ul>
In a Meteor project, I have a collection of documents which can have one of two formats. To simplify, a given document may either have a type property or a label property. I would like to display this is a multiple select element, using whichever property they have to identify them.
Here's a working example:
HTML
<body>
{{> test}}
</body>
<template name="test">
<select name="elements" size="2">
{{#each elements}}
<option value="{{id}}">{{id}}: {{type}}{{label}}</option>
{{/each}}
</select>
</template>
JavaScript
Template.test.helpers({
elements: function () {
return [
{ id: 1, type: "type" }
, { id: 2, label: "label" }
]
}
})
This happily displays:
1: type
2: label
However, this is a simplification. The actual data that I want to display is more complex than this. I would rather not put all the concatenation logic into the HTML template. I would much rather call a JavaScript function, passing it the current document, and get the string to display. Something like:
<option value="{{id}}">{{functionToCall_on_this}}</option>
How could I do this with Meteor?
Answering my own question, I have found that I can change my template to include this line...
<option value="{{id}}">{{id}}: {{string}}</option>
And my helpers entry to use the map method:
Template.test.helpers({
elements: function () {
data = [
{ id: 1, type: "type" }
, { id: 2, label: "label" }
]
return data.map(function (item, index, array) {
var string = item.type || item.label
return { id: item.id, string: string }
})
}
})
I'd like to generate a template with something like this:
<ul>
<li>A B C D</li>
<li>E F G H</li>
</ul>
The data i'm providing to the template is a cursor that has 8 rows (one for each letter).
I know I could do a fetch and split the results in groups of 4, but if I'm not mistaken, the fetch is less efficient, if e.g. I update the value of A to 1, it reloads everything.
Any suggestion on how to do this is appreciated.
html:
<template name="outer">
<ul>
{{#each list}}
{{> quadItem }}
{{/each}}
</ul>
</template>
<template name="quadItem">
<li>{{#each quad}}{{item}} {{/each}}</li>
</template>
js:
Template.outer.helpers({
list: function(){
var arrayOfQuads = [];
var array = myCollection.find().fetch();
for ( var i = 0; i < array.length; i += 4 ){
arrayOfQuads.push(array.slice(i,i+3));
}
return arrayOfQuads;
}
});
Template.quadItem.helpers({
quad: function(){
return this; // data context should be one row of arrayOfQuads
},
item: function(){
return this; // data context should be one element of a quad
}
});
This is very quick and dirty, I'm sure it can be made more elegant.
For example see how to make an array element selectable by index in blaze.
In Meteor, I have an app where I make a list of items grouped by tags, where non-tagged items come first and then tagged items are hidden under a "tag header" drop down.
I haven't touched this app since 0.8 came out, and I was using a block helper in a template which worked fine in pre-0.8...
See working jsfiddle here
Handlebars.registerHelper('eachItem', function(context, options) {
var ret = "";
for(var i=0, j=context.length; i<j; i++) {
if(!context[i].tag){
ret = ret + options.fn(context[i]);
} else {
if(i===0||!context[i-1].tag ||context[i-1].tag !== context[i].tag){
ret = ret + '<li class="list-group-item"><a data-toggle="collapse" data-target="#'+ context[i].tag +'"> Items tagged '+context[i].tag + '</a></li><div id="'+context[i].tag+'" class="collapse">';
}
ret = ret + options.fn(context[i]);
if(i+1<j && context[i+1].tag !== context[i].tag){
ret = ret + '</div>';
}
}
}
return ret;
});
But I'm struggling a bit to translate this into post-0.8 Meteor
The inserted HTML must consist of balanced HTML tags. You can't, for example,
insert " </div><div>" to close an existing div and open a new one.
One idea I had was to render the non-tagged items and also the containers in a vanilla {{#each}} loop (with 2 different templates), and then do something like this
Template.myListContainer.rendered = function(){
_.each(tags, function(tag){
var tagged_items = _.filter(items, function(item){ return item.tag == tag; });
_.each(tagged_items, function(item){
UI.insert(UI.RenderWithData(listItemTemplate, { item : item }), tagContainer);
});
});
}
Is there a simpler way to do this ? If the items come from a collection, will they keep their reactivity ?
Many thanks in advance !
If you haven't already, it's worth reading the Meteor wiki on migrating to blaze.
Anyhow, this one seems difficult to implement as a straight iteration.
If you don't need them in a specific order, I would just list items w/ and w/o tags, eg:
Template.example.helpers({
dataWithoutTags: function(){
return items.find({tag:{exists: false}});
},
tagList: function(){
// create a distinct list of tags
return _.uniq(items.find({tag:{exists: true}}, {tag: true}));
},
dataForTag: function(){
// use `valueOf` as `this` is a boxed-string
return items.find({tag: this.valueOf()});
}
});
Template:
<template name="example">
<div class='panel panel-default'>
<ul class='list-group'>
{{#each dataWithoutTags}}
<li class='list-group-item'>{{name}}</li>
{{/each}}
{{#each tagList}}
<li class="list-group-item">
<a data-toggle="collapse" data-target="#{{this}}"> Items tagged {{this}}</a>
</li>
<div id="{{this}}" class="collapse">
{{#each dataForTag}}
<li class='list-group-item'>{{name}}</li>
{{/each}}
</div>
{{/each}}
</ul>
</div>
</template>
If you do need them in a specific order - eg. grouping only consecutive items (as per your example). The only option would be to pre-process them.
eg:
Template.example.helpers({
itemsGrouped: function(){
dataGroups = [];
currentGroup = null;
items.find({}, {sort: {rank: 1}}).forEach(function(item){
if (currentGroup && (!item.tag || currentGroup.tag != item.tag)){
dataGroups.push(currentGroup);
currentGroup = null;
}
if (item.tag){
if (!currentGroup){
currentGroup = {
group: true,
tag: item.tag,
items: [item]
};
} else {
currentGroup.items.push(item);
}
} else {
dataGroups.push(item);
}
});
if (currentGroup){ dataGroups.push(currentGroup); }
return dataGroups;
}
});
Template:
<template name="example">
<div class='panel panel-default'>
<ul class='list-group'>
{{#each itemsGrouped}}
{{#if group}}
<li class="list-group-item">
<a data-toggle="collapse" data-target="#{{tag}}"> Items tagged {{tag}}</a>
</li>
<div id="{{tag}}" class="collapse">
{{#each items}}
<li class='list-group-item'>{{name}}</li>
{{/each}}
</div>
{{else}}
<li class='list-group-item'>{{name}}</li>
{{/if}}
{{/each}}
</ul>
</div>
</template>
If you log the value you are returning from the eachItem helper, you will see you are not closing the last div element. Fixing that may get it working again. However, you have div elements as direct children of the ul element, which you are not supposed to according to the HTML5 specification:
Permitted contents: Zero or more li elements
Also, I think it would be a better option for you to prepare your context in a format that makes it easier to let simple templates take care of the rendering. For example, using the same data from your jsfiddle, suppose you had it in the following format:
tagGroups = [{
names: ["item1", "item2"]
},{
tag: "red",
names: ["item3", "item4"]
},{
tag: "blue",
names: ["item5", "item6", "item7"]
}]
The following templates would give you the expected result in a valid html format. The ending result is a bootstrap panel containing collapsible list-groups:
<template name="accordion">
<div class="panel panel-default">
{{#each tagGroups}}
{{> tagGroup}}
{{/each}}
</div>
</template>
<template name="tagGroup">
{{#if tag}}
{{> namesListCollapse}}
{{else}}
{{> namesList}}
{{/if}}
</template>
<template name="namesList">
<ul class="list-group">
{{#each names}}
<li class="list-group-item">{{this}}</li>
{{/each}}
</ul>
</template>
<template name="namesListCollapse">
<div class="panel-heading"><a data-toggle="collapse" data-target="#{{tag}}">Items tagged {{tag}}</a></div>
<ul id="{{tag}}" class="panel-collapse collapse list-group">
{{#each names}}
<li class="list-group-item">
{{this}}
</li>
{{/each}}
</ul>
</template>
And here is an example of a helper to transform your data so you can make a quick test to see it working:
Template.accordion.tagGroups = function () {
var data = [{
name : 'item1'
},{
name : 'item2'
},{
name : 'item3',
tag : 'red'
},{
name : 'item4',
tag : 'red'
},{
name : 'item5',
tag : 'blue'
},{
name : 'item6',
tag : 'blue'
},{
name : 'item7',
tag : 'blue'
}];
data = _.groupBy(data, 'tag');
data = _.map(data, function (value, key) {
var obj = {};
if (key !== 'undefined') {
obj.tag = key;
}
var names = _.map(value, function (item) {
return item.name;
});
obj.names = names;
return obj;
});
//console.dir(data);
return data;
};
And yes, if the items come from a collection, you will get reactivity by default.
I want to fetch the last item of my JSON file. So I took a look at Getting the last element from a JSON array in a Handlebars template and tried to implement it. So far it gives me the number of the last entry but I need the options as well but dont know how to do it?
This is from the example mentioned
Handlebars.registerHelper("last", function(array, options) {
return array[array.length-1];
});
I tried to do:
Handlebars.registerHelper("last", function(array, options) {
if (array[array.length-1]) return options.fn(this);
return options.inverse(this);
});
My JSON files structure is:
releases: [{
"title" : "some title",
"releaseDate" : "2014-08-04"
},
"services": [{
"name" : "spotify",
"link" : "some link"
},
{
"name" : "itunes",
"link" : "some link"
}]
]
so my Handlebars template looks like:
{{#each releases}}
{{#last releaseDate}}
{{#each services}}
{{#equal name "Spotify" }}
{{/equal}}
{{#equal name "Itunes" }}
{{/equal}}
{{/each}}
{{/last}}
{{/each}}
But its not working, it displays an empty DIV
please help?
Handlebars already has #first and #last pseudo-variables. See docs on iterations and built-in helpers.
Example use case:
textArray = ["First", "N-1", "Last"]
<span>{{#each textArray}}{{#if #last}}{{this}}{{/if}}{{/each}}</span>
Result: <span>Last</span>
I would use an iterator helper for this.
Handlebars.registerHelper('layoutReleases', function (rows, options) {
var buffer = [], lastRow;
if (rows && rows.length > 0) {
lastRow = rows[rows.length-1];
buffer.push(options.fn(lastRow));
}
return buffer.join('');
});
Template:
{{#layoutReleases releases}}
{{releaseDate}}
{{#each services}}
{{#equal name "Spotify" }}
{{/equal}}
{{#equal name "Itunes" }}
{{/equal}}
{{/each}}
{{/each}}