Lets say I have the following JSON and handlebars.js template :
JSON
{
rootPath: '/some/path/',
items:[ {
title: 'Hello World',
href: 'hello-world'
}, {
title: 'About',
href: 'about'
}, {
title: 'Latest News',
href: 'latest-news'
}
}
Template
<script id="test-template" type="text/x-handlebars-template">
<ul class="nav">
{{#each items}}
<li>{{title}}</li>
{{/each}}
</ul>
</script>
The template above works, until I want to filter items - lets say to have 2 lists one odd and the other even, here's a simple template for odd :
<script id="test-template" type="text/x-handlebars-template">
<ul class="nav">
{{#each items}}
{{#isOdd #index}}
<li>{{title}}</li>
{{/isOdd}}
{{/each}}
</ul>
</script>
And the registered helper :
// isOdd, helper to identify Odd items
Handlebars.registerHelper('isOdd', function (rawValue, options) {
if (+rawValue % 2) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
The helpers work as expected and only the Odd items are rendered, however the reference to the parent context becomes lost, so the {{../rootPath}} directive ~~fails to render~~ renders an empty value.
Is there a way to pass the Parent context through the block Helper?
Change this:
<a href="{{../rootPath}}{{href}}">
to this:
<a href="{{../../rootPath}}{{href}}">
Why? because the if statement is in an inner context so first you need to go up a level and that's why you have to add ../
See more details in:
https://github.com/wycats/handlebars.js/issues/196
Related
For whatever reason I am unable to solve this issue with countless hours of troubleshooting. I have some simple helpers working with a Bootstrap 3 nav-tabs list.
I want to render a different template based on which list item is active. Here are my helpers:
Template.Profile.helpers({
'personal':function(){
if($('.profile-info').hasClass('active')) {
return true;
} else {
return false;
}
},
'groups':function(){
if($('.profile-groups').hasClass('active')) {
return true;
} else {
return false;
}
},
'commitments':function(){
if($('.profile-commitments').hasClass('active')) {
return true;
} else {
return false;
}
}
});
And here is my HTML:
<ul class="nav nav-tabs">
<li class="active profile-info">Personal Info</li>
<li class="profile-groups">Groups</li>
<li class="profile-commitments">Commitments</li>
</ul>
{{#if personal}}
{{> ProfilePersonal}}
{{else}}
{{#if groups}}
{{> ProfileGroups}}
{{else}}
{{> ProfileCommits}}
{{/if}}
{{/if}}
The helpers will not be re-run when you click a tab, as there is no reactive data change to invalidate the computation.
A more Meteor-ish approach would be to add a reactive variable to hold the tab state and change that in an event listener.
<template name="Profile">
<ul class="nav nav-tabs">
{{#each tabs}}
<li class="{{isActive #index}} profile-{{name}}">{{title}}</li>
{{/each}}
</ul>
{{> Template.dynamic template=tpl}}
</template>
#index references the index of the current loop, and it's provided as an argument to the isActive helper.
Then, your JavaScript file can include a definition for the tabs and the handling code:
var tabs = [{
idx: 0,
name: "info",
title: "Personal Info",
template: "ProfilePersonal"
}, {
idx: 1,
name: "groups",
title: "Groups",
template: "ProfileGroups"
}, {
idx: 2,
name: "commitments",
title: "Commitments",
template: "ProfileCommits"
}];
The tabs are a plain JS array. The following code uses them in the template's context:
Template.Profile.helpers({
// get current sub-template name
tpl: function() {
var tpl = Template.instance();
return tabs[tpl.tabIdx.get()].template;
},
// get the tabs array
tabs: function() {
return tabs;
},
// compare the active tab index to the current index in the #each loop.
isActive: function(idx) {
var tpl = Template.instance();
return tpl.tabIdx.get() === idx ? "active" : "";
}
});
Template.Profile.events({
'click .nav-tabs > li': function(e, tpl) {
tpl.tabIdx.set(this.idx);
}
});
Template.Profile.onCreated(function() {
this.tabIdx = new ReactiveVar();
this.tabIdx.set(0);
});
When the template is created (onCreated()), a new reactive variable is added as an instance variable. This variable can then be accessed in helpers and set in event handlers.
The event handler receives the event object and template instance as parameters and has the data context set as the this pointer; therefore, tpl.tabIdxrefers the reactive variable and this refers to the object that represents the clicked tab (for example,
{
idx: 0,
name: "info",
title: "Personal Info",
template: "ProfilePersonal"
}
for the first tab, as this was the template's data context when the first tab was rendered.
The helper functions get the Template instance using a call to Template.instance(). Then, it queries the value of the reactive array.
This creates a computation in a reactive context (helpers are reactive contexts and they are rerun when the computation they create is invalidated, and that happens when an Mongo cursor, or a reactive variable that is read in the computation is changed).
Therefore, when the reactive variable is set in the event handler, the helpers are re-run and the template reflects the new value.
These are all fundamental to Meteor and are explained in the full Meteor documentation and in many resources.
This question already has answers here:
How do I check if an array includes a value in JavaScript?
(60 answers)
Closed 7 years ago.
I have two collections:
Group = {
users: [Array_of_User]
}
User = {
name: _string_
}
I'm listing groups ans I'm trying to know in the template if a user is in the groups:
mytemplate.js
Template.mytemplate.helpers({
groups: function(){
return Groups.find();
},
currentUsername: 'test'
});
mytemplate.html
<template name="main">
<ul>
{{#each groups}}
<li>
{{#if [the group contains currentUsername] }}
contains
{{else}}
doesn't contain
{{/if}}
</li>
{{/each}}
</ul>
</template>
The question is: what can I put on the helpers and instead of [the group contains currentUsername] to make it work?
Also, I'm not saying this is the way to do it. I'm open to any suggestions even if it means I have to change a lot.
You could use the Underscore.js function _.findWhere(list, properties) to check whether the group contains the username:
if (Meteor.isClient) {
Template.main.helpers({
groups: function() {
return Groups.find();
},
currentUsername: 'Matthias',
isInGroup: function(username) {
return !!_.findWhere(this.users, {
name: username
});
}
});
}
<template name="main">
<ul>
{{#each groups}}
<li>
{{#if isInGroup currentUsername}}
contains
{{else}}
doesn't contain
{{/if}}
</li>
{{/each}}
</ul>
</template>
if (Meteor.isServer) {
Meteor.startup(function() {
Groups.insert({
users: [{
name: "Matthias"
}, {
name: "Angie"
}]
});
});
}
Here is a MeteorPad.
Within your each block, your data context becomes the current group that is being iterated over. Therefore you can write a helper method that references that current data context like this:
userInGroup: function(username) {
var userInGroup;
this.forEach(function(groupUsername) {
if (username == groupUsername) {
userInGroup = true;
}
};
return userInGroup;
}
'this' within the userInGroup template helper references the current group as long as you use the helper within an a group iteration.
You can then use the helper like this:
<template name="main">
<ul>
{{#each groups}}
<li>
{{#if userInGroup currentUsername}}
contains
{{else}}
doesn't contain
{{/if}}
</li>
{{/each}}
</ul>
</template>
I try to integrate rubaxa:sortable to make my list sortable.
client/helpers.js
Template.getElements.helpers({
children: function() {
return Articles.find({ parent: this._id }, {sort: {order: 1}});
}
});
server/publications.js
Meteor.publish('articles', function() { return Articles.find({}, {sort: {slug: 1}}); });
Sortable.collections = 'articles';
template
<template name="getElements">
<ul class="sortable">
{{#each children}}
{{#sortable items=Articles sortField="order"}}
<li data-id="{{_id}}"><input type="text" name="keyword" value="{{title}}"></li>
{{/sortable}}
{{/each}}
</ul>
</template>
In the documentation (https://atmospherejs.com/rubaxa/sortable) I see the info:
Client:
{{#sortable items=<collection|cursor|array> sortField="order"}}
Server:
Sortable.collections = <collectionName>; // the name, not the variable
So what am I doing wrong? Right now no list-element is beeing shown.
I'm not an expert but based on integration this will fix your issues.
Server JS
Update the declaration to include the collation name in [].
Sortable.collections = ['articles'];
HTML Template
remove the following:
{{#each children}}
{{/each}}
Regards,
Vince
I am trying to use Iron Router to call a page and filter results by the route. Effectively, the application allows you to create items. Items include an array that can have 0 to many tags:
Item: {
_id: <assigned by application>,
itemTags: []
}
The dropdown list on the navbar collects all tags from the items, does some cleaning, and then drops them into the dropdown menu:
// HTML
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<ul class="nav navbar-nav">
<li class="dropdown">
Select List to View <span class="caret"></span>
<ul class="dropdown-menu" role="menu">
<li>All List Items</li>
<li class="divider"></li>
{{#each uniqueTags}}
<li>{{this}}</li>
{{/each}}
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
// JS
Template.header.helpers({
uniqueTags: function() {
var allItems = Items.find({checked: false});
var uniqueTags = allItems.map((function(allItems) {return allItems.itemTags;}));
uniqueTags = _.flatten(uniqueTags);
uniqueTags = _.compact(uniqueTags);
uniqueTags = _.uniq(uniqueTags);
uniqueTags = _.sortBy(uniqueTags, function (name) {return name;});
return uniqueTags;
}
});
The router then sends you to "/list/". Everything up to this point works great. But then it falls apart. My problem is twofold:
1) How do I appropriately grab the data context of just the items that have the tag listed somewhere in the array?
2) How do I display those items on the "list" page? I have zero idea what should be in list.js to handle the returned data context
My current code is below, but it's hacked up and clearly not working :)
// JS - router.js
Router.route('/list/:tagName', {
name: 'list',
data: function() {
return Items.find({$and: [
{checked: false}, {listItem: true}, {itemTags: {$in: ['this.params.tagName']}}
]});
}
});
// HTML - list.html
<template name="list">
<h3><span class="label label-default view-list">List</span></h3>
{{#each listItems}}
{{> item}}
{{/each}}
</template>
// JS - list.js
Template.list.helpers({
listItems: function() {
<somehow use the returned data context>
}
});
Thanks for your help!
This would work
Router.route('/list/:tagName', {
name: 'list',
data: function() {
return { 'listItems' : Items.find({$and: [
{checked: false}, {listItem: true}, {itemTags: {$in: [this.params.tagName]}}
]}) };
}
});
// HTML - list.html
<template name="list">
<h3><span class="label label-default view-list">List</span></h3>
{{#each listItems}}
{{> item}}
{{/each}}
</template>
// You have to write a item template
// JS - list.js
Template.list.helpers({
//listItems: function() {
// this helper is not required
// you could access the data by this.data.listItems
//<somehow use the returned data context>
//}
});
I am trying to figure out why filtering on a controller's property directly is not working as expected.
controller:
var VideoIndexController = Em.ArrayController.extend({
genreFilter: "",
actions: {
filterGenre: function(genre) {
this.set("genreFilter", genre);
}
},
genres: function() {
var genres = this.get('content').map(function(video) {
return video.get("GenreName");;
}).filter(function(genre, index, arr) {
return arr.indexOf(genre) == index;
});
console.log("genres", genres);
return genres
}.property("content"),
filteredContent: function() {
var content = this.get("arrangedContent");
if (!Ember.isEmpty(this.get("genreFilter"))) {
return content.filterBy("GenreName", this.get("genreFilter").valueOf());
} else {
return content;
}
}.property("arrangedContent", "genreFilter")
});
console.log for genres:
["Youth/Children", "Movies", "Actuality/Information", "Series", "Shows", "Sport", nextObject: function, firstObject: undefined, lastObject: undefined, contains: function, getEach: function…]
0: "Youth/Children"
1: "Movies"
2: "Actuality/Information"
3: "Series"
4: "Shows"
5: "Sport"
__ember1384338760148_meta: Meta
_super: undefined
length: 6
__proto__: Array[0]
template:
{{outlet}}
<a href="" {{action "filterGenre"}}>All</a>
{{#each genres}}
<a href="" {{action "filterGenre" this}}>{{this}}</a>
{{/each}}
<a href="" {{action "sortAZ"}}>A-Z</a>
<a href="" {{action "sortZA"}}>Z-A</a>
<a href="" {{action "sortSuggested"}}>Suggested</a>
<section >
{{#each filteredContent}}
{{#linkTo 'catchup/item' this}}
<div class="catchup-list-item">
{{#if thumbnailUrl}}
<div>
<img {{bindAttr src="thumbnailUrl"}} />
</div>
{{/if}}
<div {{bindAttr class=":title validDownloadUrl::invalid"}}>
<div>{{ProgramName}}</div>
<div>{{subTitle}}</div>
</div>
</div>
{{/linkTo}}
{{/each}
</section>
notice the 2nd parameter in content.filter()
if I call this like:
content.filterBy("GenreName", this.get("genreFilter"));
then the returned array is always empty.
adding .valueOf() gives me the expected output. This seems pretty cumbersome and error prone.
edit: note that I am using arrangedContent, as I have some snipped actions that perform sorting
edit2: updated to include template
edit3: added genres array console log
The filteredContent should be a computed property of arrangedContent.[], not arrangedContent. The arrangedContent is an array, and you need to tell Ember to list for array changes (items added, items removed).
Having said that I would guess that the {{action "filterGenre" genre}} is not passing the right property. I guess it should be a string, but it passes an Object. That's why when you call valueOf() it produces the expected result. The filterBy function expects a string and in that case the JS interpreter passes it through toString() first.
It would be helpful if you can post the actual template where the action is called, or give some log output for the value of genreFilter.