Following the documentation on subscribing to change events for a single entity is working easy peasy -
var token;
var myEntity = ko.observable();
if (token == null) {
token = myEntity().entityAspect.propertyChanged.subscribe(function (changeArgs) { trackChanges(changeArgs); });
}
But if I want to subscribe to an observableArray and keep track of the tokens
var tokens = ko.observableArray();
var myEntitys = ko.observableArray();
if (tokens().length === 0) {
ko.utils.arrayForEach(myEntitys(), function (entity) {
var etoken = entity.entityAspect.propertyChanged.subscribe(function (changeArgs) { trackChanges(changeArgs); });
tokens.push(etoken);
});
}
console.log(tokens());
The subscriptions are working fine but each of the tokens are equal to the same value
//This works fine
myEntity().entityAspect.propertyChanged.unsubscribe(token);
//**This does not work because myEntitys does not have an entityAspect, of course **
ko.utils.arrayForEach(tokens(), function (token) {
myEntitys().entityAspect.propertyChanged.unsubscribe(token);
});
//This works, but I am not sure why because token was set above to equal a single entity (myEntity())
ko.utils.arrayForEach(myEntitys(), function (entity) {
entity.entityAspect.propertyChanged.unsubscribe(token);
});
Is there a way to either unsubscribe EVERYTHING when leaving a view or a proper method to do what I am attempting above? If I reload the same view again it is double-subscribed to propertyChange events.
We were able to reproduce the issue and are investigating the problem.
Related
I'm building an application in Angular with Firebase and one aspect of it is one-to-one chat. I'm querying Firebase to see if a chat room exists already between the user currently accessing the application and the user they are attempting to chat with. If it exists, I am trying to apply that room to the scope as the selected room. I'm using "Messages" service to run the query.
this.roomQuery = function(user1ID, user2ID) {
roomsRef.orderByChild("user1").equalTo(user1ID).on("child_added", function(snapshot) {
if (snapshot.val().user2 == user2ID) {
self.selectedRoom = snapshot.key();
console.log(self.selectedRoom);
} else {
self.selectedRoom = null;
}
})
}
and in my controller I am using:
$scope.$watch(
function(){ return Messages.selectedRoom },
function(newValue, oldValue) {
$scope.selectedRoom = newValue;
}
)
This $scope.$watch method has worked for me with everything else and it seems to sometimes work in this case. The console log always prints out the correct value for Messages.selectedRoom, but the $scope.selectedRoom sometimes does not update. Any idea what is happening here? I'm very confused. If it's logging to the console properly, shouldn't it be updated in the scope?
Angular's $digest is unaware of when a your Firebase query completes. You might find it easier to use AngularFire in this case.
this.roomQuery = function(user1ID, user2ID) {
var query = roomsRef.orderByChild("user1").equalTo(user1ID);
return $firebaseObject(query).$loaded();
};
this.roomQuery("1", "2")
.then(function(data) {
// do your check here
});
The $firebaseObject() takes in a ref or a query and knows when to call digest on your behalf.
You might want to check out using resolve in the router to inject the roomQuery into the router, since it returns a promise with .$loaded().
David got me to the solution I needed. For anyone with a similar issue, here is how I implemented it:
this.roomQuery = function(user1, user2) {
var query = roomsRef.orderByChild("user1").equalTo(user1ID)
return $firebaseArray(query).$loaded();
}
I used $firebaseArray instead of Object and in my controller:
$scope.getRoom = function() {
Messages.roomQuery($scope.user1.id, $scope.user2.$id).then(function(data)
{
$scope.data = data;
for(var i=0, len = data.length; i < len; i++){
if (data[i].user2 == $scope.user2.$id) {
$scope.selectedRoom = data[i].$id;
}
}
}
)
}
Apologies for the variable names being a little confusing. I altered them for the sake of this post.
I have an application in AngularJS and I want to implement an autocomplete input for a certain list of programs.
My problem is that I have lots of programs in my database and I don't want to load them all when the page loads. Instead I load pages and have a button that loads the next page when clicked.
scope.loadPrograms = function() {
Programs.getPage($scope.page)
.success(function(data) {
$scope.allprograms.push.apply($scope.allprograms, data.campaigns);
$scope.page++;
if(data.pagination.pages < $scope.page) {
$scope.page = -1;
}
})
.error(function(data){
alert('There has been an error. Please try again later!');
});
}
and the button
<md-button ng-click="loadPrograms()" ng-show="page != -1">Load more data</md-button>
So this approach makes me do a request everytime I write/delete a letter in the autocomplete input, given the fact that I don't have all the program loaded on $scope. Is it ok to make so many request? Is there another approach?
Thanks.
EDIT
Ok so now I put a delay on the autocomplete, but the method doesn't work anymore.
// Search for programs
scope.querySearch = function(query) {
if (typeof pauseMonitor !== 'undefined') {
$timeout.cancel(pauseMonitor);
}
pauseMonitor = $timeout(function() {
var results = query ? scope.allprograms.filter(createFilterFor(query)) : [];
return results;
}, 250);
};
// Create filter function for a query string
function createFilterFor(query) {
var lowercaseQuery = angular.lowercase(query);
return function filterFn(programs) {
return (programs.name.toLowerCase().indexOf(lowercaseQuery) != -1);
};
};
It enters in the createFilterFor method, finds a good match but doesn't show it anymore.
If you need to retrieve a set of words for the purpose of auto completion from a large database, one simple trick is to use $timeout with some time threshold which can detect the pauses of the user typing.
The idea is to prevent a request being generated for every key. You look for a pause in the user typing pattern and make your request there for the letters typed. This is a simple implementation of this idea in your key handler.
function processInput(input) {
if (typeof pauseMonitor !== 'undefined') {
$timeout.cancel(pauseMonitor);
pauseMonitor = $timeout(function() {
//make your request here
}, 250);
}
Take a look at ng-model-options
you can set a debounce time and some other interesting things.
ng-model-options="{ debounce: '1000' }"
Above line means the input value will be updated in the model after 1 sec
I'm trying to represent multiple selects with its selected values from backend JSON to knockout view model.
And it's needed to retrieve this JSON when each select is changed, first time - all is ok, but if I apply mapping again (ko.mapping.fromJS(test_data, ViewModel)), all subscriptions are lost does anyone know how to avoid this situation?
jsfiddle (I don't know why selects don't have its values, without jsfiddle - all is ok):
http://jsfiddle.net/0bww2apv/2/
$(ViewModel.attributes()).each(function(index, attribute) {
attribute.attribute_value.subscribe(function(name) {
console.log('SUBSCRIBE', name);
var send_data = {};
$(ViewModel.attributes()).each(function (index, attribute) {
send_data[attribute.attribute_name.peek()] = attribute.attribute_value.peek();
if (attribute.attribute_value() === null) {
send_data = null;
return false;
}
});
if (send_data) {
console.log('REQUEST TO BACKEND: ', ko.toJSON(send_data));
ko.mapping.fromJS(test_data, ViewModel);
// subscriptions is lost here !
}
});
});
At last I've solved my own question with knockout.reactor plugin,
If we remove all auxiliary constructions, it will look like:
var ViewModel = ko.mapping.fromJS(test_data);
ko.applyBindings(ViewModel);
ko.watch(ViewModel, { depth: -1 }, function(parents, child, item) {
// here we need to filter watches and update only when needed, see jsfiddle
ko.mapping.fromJS(test_data2, {}, ViewModel);
});
This way we update selects and don't have troubles with subscription recursions.
full version (see console output for details): http://jsfiddle.net/r7Lo7502/
On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).
Tracker.autorun(function() {
DATA.find().observeChanges({
added: function(id, doc) {
console.log(doc);
}
});
});
This code is being called on the server. Every time the meteor server starts, the added function fires for every single item in the database. Is there a way to have the added callback fire only when new items are added?
added will be called for every document in the result set when observeChanges is first run. The trick is to ignore the callback during this initialization period. I have an expanded example in my answer to this question, but this code should work for you:
(function() {
var initializing = true;
DATA.find().observeChanges({
added: function(id, doc) {
if (!initializing) {
console.log(doc);
}
}
});
initializing = false;
})();
Note that Tracker.autorun is a client-only function. On the server I think it only ever executes once.
I struggled with this for a long time. For some reason, David's answer did not work for me - it was firing after the initializing variable was set to false.
This pattern from Avi was successful for me:
var usersLoaded = false;
Meteor.subscribe("profiles", function () {
// at this point all new users sent down are legitimately new ones
usersLoaded = true;
});
Meteor.users.find().observe({
added: function(user) {
if (usersLoaded) {
console.log("New user created: ", user);
}
}
});
Since it is initialization issue, you can do this.
var observerOfMessages = Messages.find({}).observe({
added: function(doc){
if(!observerOfMessages) return;
console.log(doc)
}
});
This is more elegant actually.
Provide a selector for the query which does not match old items. If using mongo ObjectID as _id you could query for items that have _id greater than the latest item's:
const latest = DATA.findOne({}, {sort: {_id: -1}})
DATA.find({_id: {$gt: latest._id}}).observeChanges({
added: function() { ... }
})
Or with createdAt timestamp:
const currentTime = new Date()
DATA.find({createdAt: {$gt: currentTime}}).observeChanges({
added: function() { ... }
})
Here's another way to solve this:
Meteor.subscribe('messages', function() {
var messages = Messages.find();
var msgCount = messages.count();
messages.observe({
addedAt: function(doc, atIndex) {
if(atIndex > (msgCount - 1)) console.log('added');
}
});
});
Should only fire for docs added after the existing amount is delivered. It's important that this goes in an onReady callback for Meteor.subscribe so that the msgCount changes as your subscription does... if for example, you're paginating your subscriptions.
cursor.observe() documentation