I'm building a Durandal app, and the view I'm currently working on has two <select> boxes. I've got both of them bound to a ko.observableArray and their value to another ko.observable as follows:
<select data-bind="options: dateOptions, optionsText: 'display', value: selectedDate></select>
<select data-bind="options: buyerOptions, optionsText: 'display', value: slectedBuyer"></select>
The second one is dependent on the value of the first one, so I'm populating them at different times. For the first, I'm querying my data source during the activate() call and then passing the data to a separate method to populate the array the data (in the form of simple JS objects) when the promise returned by the request is resolved:
var populateDateOptions = function(dates) {
$.each(dates, function() {
dateOptions.push({
display: dateToString(this.pbDateOpt),
date: this.pbDateOpt
});
});
};
That works fine - the <select> has values ready for me when the view is rendered. But, after that, I can't get either <select> to respond to changes in their respective observable arrays. The next <select> is populated in a nearly-identical fashion once a value is selected in the first <select>, and I can verify that the buyerOptions array is indeed being populated, but the <select> doesn't change. I also tried adding a value to the first <select> by pushing an object into its array via dev tools and get the same result: the array is updated, but the UI doesn't change.
I've used the "options" binding in several other apps and never had this issue before, but this is my first Durandal app so I'm wondering if maybe there's something I'm missing?
Thanks!
Update
I added some more bound elements to the view and none of them are working, so there must be something weird going on with the composer. Not sure where to start looking for an issue (the viewmodel and view are very similar in structure to another pair that is working fine). Any tips?
Just as a reference, this isn't a Durandal issue - this is a Knockout issue.
Also, a way that I have found most efficient to do this is the following the same way you have it -
<select data-bind="options: dateOptions, optionsText: 'display', value: selectedDate></select>
<select data-bind="options: buyerOptions, optionsText: 'display', value: selectedBuyer"></select>
but in your view model make the buyerOptions dependent directly on the dateOptions like so -
var buyerOptions = ko.computed(function () {
var opts = ko.observableArray();
if (!selectedDate()) { opts.push(display: '<Select a Date>'); }
else { opts(getBuyersOptions(selectedDate().id()); }
return opts;
});
This way if your selectedDate observable is empty (one hasn't been selected) then no buyerOptions appear, but if you select a date it will populate it based off some value in selectedDate, like Id. It will also automatically update whenever a new date is chosen, without you having to explicitly tell it to with JavaScript or w/e
Turning on Durandal's debug mode by setting system.debug(true) in main.js helped me discover some code errors that weren't presenting themselves via console warnings. With those resolved, everything bound/worked correctly.
Lesson learned - leave debug mode on when you're in development!
Related
I've come across this situation several times now and I realise I'm not really confident about the 'meteor/right' way to handle it.
Suppose I have a form with several parts - each represented by a template - and within each part there are more templates representing eg. datepickers etc.
<template name='myForm'>
{{>partOne}}
{{>partTwo}}
<button class='submit'>Submit</button>
</template>
<template name='partOne'>
{{>widget}}
{{>widget}}
</template>
<template name='widget'>
<input class='datepicker' />
</template>
I want to keep track of my form as the user fills it out - on the level of the 'myForm' template - but all the events are happening at the level of 'widget'.
One solution I keep seeing (e.g. in this SO answer) is to just put everything in the global Session variable. Like so
Template.widget.events({
'click .select' : function(event, template){
var name = template.data.name;
Session.set(name, $(event.currentTarget).val());
}
});
And then in myForm I should do something like this
Template.myForm.rendered = function(){
Tracker.autorun(function(){
var name = Session.get('name');
// do something
});
}
But as my forms are getting more complicated, I find this is really turning into a mess on the myForm template level, all while filling up my Session variable with data that isn't really application-global.
I'd be really grateful for any ideas on how others deal with this ! Keeping templates and widgets modular while still being able to follow and react to their triggered events from parent templates...
You're not alone in feeling like something just isn't right. This is one of the reasons there's a lot of talk about a Blaze 2. Here's what I do:
Create an app global namespace (e.g. G = {}). I usually use the first letter of the app name & do this in lib/config/_namespace.js
Put your collections in G.Collections or G.C,
Put your shared functions in G.Fx, etc...
Put your template vars in G.T.
Then, save that variable to G.T.varName. In doing so, you can use it in rendered as well as events and helpers. As a perk, it's super easy to find all your "globals" because they're all in the G object. Additionally, you can now 'use strict' again.
Then, to keep it clean:
Template.parentTemplate.destroyed = function() {
G.T = {};
};
So if you need reactivity, just make a ReactiveDict:
Template.parentTemplate.created = function() {
G.T.RD = new ReactiveDict();
};
You can use a file-level ReactiveVar or ReactiveDict, instead of the Session object.
In an application currently I am loading my views using routers like below
router('menu/:item', function (item) {
app.uiHandler.toggleMenuSelected('menu', item);
// The below method updates the view with selected menu item's model.
app.channel.publish('menu', item);
});
Currently each menu item shares same data object. But the master view is replaced with new html based on each menu selection.
I am thinking to have instance like below for each menu item
var ractive = new Ractive({
el: 'container', // el is same for all instances.
template: '<p> I am {{selection}}, after {{prevSelection}}!</p>',
data: { selection: 'Home', prevSelection: 'Profile' }
});
But here my doubt is as I will be invoking each instance to render the view into 'container' whenever hash is changed how to clear all the two way data bindings created when master view is replaced with new html. Please help me on this.
If I am handling in wrong way, what would be the best way to handle the same.
Note : My question might sound like stupid, but I am looking for clarification on this :)
how to clear all the two way data bindings created when master view is replaced with new html
You can use teardown() to destroy the ractive instance, but if you don't ractive will do that automatically as soon as you try to render a new instance into the same container.
That said, it's probably better to have one instance and only update the data. I.e. when the section changes call ractive.set({ selection: 'Profile', prevSelection: 'Home' }).
In my meteor app I need to load an array of items corresponding to the item clicked.As I'm new to meteor, I'm held up here.Here is my code.
Template.templatename.events({
'click .showdiv' : function()
{
Template.templatename.vname = function () {
return Db.find();
}
}
Can I set the variable vname dynamically by this code ? This is not working for me.
I think you're misunderstanding the notion of reactivity. A reactive data source will cause any functions which depend on it (including helpers) to rerun when its value is changed, which seems to be the behavior you're looking for here. Instead, you're rewriting the helper function itself every time an item is clicked, which kind of defeats the object of Meteor's reactive data model. Session variables could help:
Template.templatename.events({
'click .showdiv' : function() {
Session.set('vname', Db.find());
}
});
Template.templatename.vname = function () {
return Session.get('vname');
}
If you use an {{#each vname}} block in the templatename template, it will automatically update with the results of the Db.find() query when a .showdiv is clicked. If all you want to do is show the result of that query regardless of whether a click has been registered it would be as simple as:
Template.templatename.vname = function () {
return Db.find();
}
Note that it's still not clear exactly what data you're trying to populate here since the query will return a cursor (which is fine, but you need to loop through it using {{#each ...}} - use findOne if you only want one item), and its contents aren't going to depend on anything intrinsic to the click event (like which .showdiv you clicked). In the former example it will however fail to show anything until the first click (after which you would have to reset with Session.set('vname', null) to stop it showing anything again).
I'm struggling with an issue that I will explain giving a simple demo.
There's following very simple document in People Collection.
{
"_id" : "vxmLRndHgNocZouJg",
"fname" : "John" ,
"nicks" : [ "Johnny" , "Jo"]
}
Now let's consider following templates. Basically I display username and a list of nicknames with input field for adding more nicknames.
<head>
<title>test</title>
</head>
<body>
{{> name}}<br/>
{{> nicks}}
</body>
<template name="name">
<input type="text" value="{{fname}}"/>
</template>
<template name="nicks">
{{#each nicks}}
<div>{{this}}</div>
{{else}}
no nicks yet
{{/each}}
<input type="text" name="nicks"/>
<input type="submit"/>
</template>
My client javascript code is as follows:
Template.name.fname = function() {
return People.findOne({"fname" : "John"},{
transform : function(doc) {
return doc.fname;
}
});
}
Template.name.rendered = function() {
console.log('Template "name" rendered!');
}
Template.nicks.nicks = function() {
var john = People.findOne({"fname" : "John"});
if(john) return john.nicks;
}
Template.nicks.events({
'click input[type="submit"]' : function () {
var johnId = People.findOne({"fname" : "John"})._id; // demo code
People.update(johnId,{
$addToSet : {
nicks : $('input[name="nicks"]').val()
}
})
}
});
My problem is that after adding nickname (update of nicks field in a document) template name is re-rendered (I know because I console.log it). When I query People collection to provide data for name template I use transform option so changes in nicks field shouldn't have impact on name template.
Meteor docs supports this:
Cursors are a reactive data source. The first time you retrieve a cursor's documents with fetch, map, or forEach inside a reactive computation (eg, a template or autorun), Meteor will register a dependency on the underlying data. Any change to the collection that changes the documents in a cursor will trigger a recomputation.
Why template name is re-rendered then?
The template is re-rendered because you change the People collection.
When you alter the People collection, Meteor automatically assumes that everything that it provides data to needs to be recalculated. (Which your name template does via Template.name.fname.
Even though you transform the output of the cursor, the People collection has changed in some way. The query is done before the transform is used, in other words, its not the transform that is looked at but the whole collection.
Meteor thinks that perhaps your document with {'fname':'John'} may have some other field that might have changed and it needs to requery it to check (which the nicks field has been altered). The transform is then applied after the requery.
Your HTML might not actually change at this point, only if the cursor returns something different will the html be changed.
If it becomes an issue in any scenario (i.e forms being cleared or DOM being changed when it shouldn't be) you can use the {{#isolate}} {{/isolate}} blocks to ensure that only everything inside them is re-rendered and nothing outside.
Meteor promises reactive updates, so that views are auto-updated when data changes. The included leaderboard example demonstrates this. It runs fine when I test it: data is updated across several browsertabs in different browsers, as expected.
All set and go, I started coding with meteor and progress was being made, but when I tested for reactive updates across browertabs, I noticed that only after a short while the updates across tabs stopped.
I boiled down the problem to the following code, based on a new empty meteor project:
updatebug.html
<head>
<title>updatebug</title>
</head>
<body>
{{> form}}
</body>
<template name="form">
<form onsubmit="return false;">
{{#each items}}
{{> form_item }}
{{/each}}
</form>
</template>
<template name="form_item">
<div>
<label>{{name}}
<input type="text" name="{{name}}" value="{{value}}">
</label>
</div>
</template>
updatebug.js:
Items = new Meteor.Collection("items");
if (Meteor.is_client) {
Template.form.items = function () {
return Items.find();
};
Template.form_item.events = {
'blur input': function(e) {
var newValue = $(e.target).val();
console.log('update', this.name, this.value, newValue);
Items.update({_id: this._id}, {$set: {value: newValue}});
},
};
}
if (Meteor.is_server) {
Meteor.startup(function () {
if (Items.find().count() === 0) {
Items.insert({name: 'item1', value: 'something'});
}
});
}
Run in multiple browsertabs, start changing the value of the input in one tab. The other tabs will reflect the change. Goto the next tab and change the value. Repeat a couple of times.
After a while, no more updates are received by any other tabs. It seems that once a tab has changed the value, it does not receive/show any more updates.
Differences compared to the leaderboard example (since it's very similar):
The leaderboard uses no form controls
The leaderboard example does an increment operation on update, not a set
I am about to file a bug report, but want to be sure I am not doing anything stupid here, or missing an essential part of the Meteor Collection mechanics (yes, autopublish package is installed).
The issue here is input element preservation. Meteor will preserve the input state of any form field with an id or name attribute across a template redraw. The redraw is preserving the old text in your form element, because you wouldn't want to interrupt another user typing in the same field. If you remove the name attribute from the text box, each tab will update on blur.
In fact, I'm not sure why the first update works in your example. That may actually be the bug!
You can see it's not a data problem by opening the console in each browser. On each blur event you will get an updated document in every open tab. (Type Items.find().fetch())