Why aren't my template's if clauses update reactively? - javascript

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.

Related

Meteor Templates helpers and access to collections' fields [duplicate]

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>

Angular orderBy not re-running after ng-click changes order string

I have a a collection of panels each with a simple list of items that needs to either be sorted by 'computedLoad' or 'Name'. I have the following objects and methods to accomplish this generically over all of the panels (only showing one panel among many).
scope.orderBy = {
name: {
displayName: "Name",
sort: "Name",
reverse: false
},
load: {
displayName: "Load",
sort: "-computedLoad",
reverse:false
}
};
scope.selectOrder = function (panel, order) {
timeout(function () {
panel.activeOrder = order;
});
};
scope.panels = {
methods: {
activeOrder: scope.orderBy.name
}
};
I have the following html:
<div>
<ul class="nav nav-pills">
<li class="list-label"><a>Order By:</a></li>
<li ng-repeat="order in orderBy">{{order.displayName}}</li>
</ul>
<ul class="nav nav-pills nav-stacked">
<li ng-repeat="item in suite.Methods | orderBy:panel.methods.activeOrder.sort"><span class="text">{{item.Name}}</span></li>
</ul>
</div>
The selectOrder method doesn't seem to work. Any ideas? Am I missing something?
Here is an example: http://jsbin.com/puxoxi/1/
Setting panel.activeOrder happens asynchronously, so it is outside of angulars so called "digest cycle".
To make angular re-evaluate your scope, use the $apply function:
It could look like this:
scope.$apply(function() {
panel.activeOrder = order;
});

BackboneJS avoid re-rendering

I have a Backbone App with a large router. I use the Backbone Layout manager to load different layouts depending on what subpage I'm on. My problem is, that my top navigation gets rendered once again, each time the subpage gets rendered. So how can I avoid this?
My router:
routes: {
'': 'index',
'home': 'home',
':name' : 'artistchannel',
':name/' : 'artistchannel',
':name/videoes': 'artist_videos',
':name/videoes/': 'artist_videos',
':name/videoes?w=:videoid' : 'artist_videos',
':name/releases': 'artist_discography',
':name/releases/': 'artist_discography',
':name/merchandise' : 'artist_merchandise',
':name/concerts': 'artist_concerts'
},
artistchannel: function (params) {
artistController.initArtist(params.name);
},
artist_discography: function(params){
artistController.initDiscography(params.name);
},
and so on...
then I have a controller for each route (here artist and discography page):
ArtistController.prototype.initArtist = function(name) {
this.artistModel = new ArtistModule.Model({slug: name});
this.artistModel.fetch();
this.artistModel.on('sync', function(){
this.artistView = new ArtistModule.View({model: this.artistModel});
App.useLayout('artistchannel', 'artistchannel').setViews({
'.userMenu': this.acuserNav,
'.artistChannelDiv': this.artistView
}).render();
}, this);
window.scrollTo(0,0);
};
ArtistController.prototype.initDiscography = function(name) {
this.artistdiscographyModel = new ArtistDiscographyModule.ArtistDiscographyModel({slug: name});
this.artistdiscographyModel.fetch();
this.artistdiscographyModel.on('sync', function() {
this.artistDiscographyView = new ArtistDiscographyModule.View({model: this.artistdiscographyModel});
App.useLayout('artistDiscography', 'artistDiscography').setViews({
'.userMenu': this.acuserNav,
'.releasesDiv' : this.artistDiscographyView
}).render();
}, this);
window.scrollTo(0,0);
};
The same goes for concerts, merchandise etc.
All subpages (in this case artistchannel.html and artistDiscography.html) have the same menu in the HTML, which I want to avoid, so basically, its repeated code which looks like:
<ul>
<li>
Releasepage
</li>
<li>
Concertpage
</li>
etc. etc.
</ul>
So what I want that the topmenu not gets rerendered all the time. Is it possible to include all inside one single controller?
Have a Layout & ShellView/MenuView. Don't append $el of every view to body, instead use a container for each specific view. One approach can be :
<body>
<div id='menu'></div>
<div id='content'></div>
</body>
new Backbone.Router.extend({
initialize:function(){
new MenuView({el:"#menu"}).render(); //this one creates menu
},
routes:{...},
routeHandler:function(){
new ArtistVideosView({el:'#content'}).render();
}
});

Knockoutjs and changing template dynamically

I use a knockoutjs with templating plugin (Using Underscore Template with Knockout using interpolate due to asp.net)
If i have a header and a list:
<ul data-bind="template: { name: 'people' }"></ul>
<script type="text/html" id="people">
<h2>{{= hdr}}</h2>
{{ _.each(people(), function(item) { }}
<li>{{=item.name }} ({{=item.age }})</li>
{{ }); }}
</script>
also a button
<button id="bindclick">Click</button>
and a ja code where i use knockout:
ko.applyBindings({
hdr: "People",
people: ko.observableArray([{name:"name1",age: 45},{name:"name2",age: 33}])
});
How to do, that template value can be change with clicking a button instead of "Uncaught Error: You cannot apply bindings multiple times to the same element."?:
$("#bindclick").click(function() {
ko.applyBindings({
hdr: "People2",
people: ko.observableArray([{name:"name1",age: 45}])
});
});
Thanks
You should only need to call applyBindings once with a model object.
Later on in your click handler, you would simply update your model.
For example:
var theModel = {
hdr: ko.observable('People'),
people: ko.observableArray([{name:"name1",age: 45},{name:"name2",age: 33}])
};
ko.applyBindings(theModel);
$('#bindclick').click(function () {
theModel.hdr('People2');
theModel.people([{name:"name1",age: 45}]);
});
Updating the model should update your previously bound content.

Handlebars.js using ../ to reference parent Context

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

Categories

Resources