KnockoutJS and async ajax call - asynchronous

I am using SPServices and KnockoutJS to load some data on a page, specifically a list of projects and the number of tasks for each project.
I have simplified the example a bit but what I am having issues with is to get the value of the ajax call async. I solved it cheaply by using jQuery to bind the result instead of returning anything:
function ProjectModel(title, id) {
var self = this;
self.id = id;
self.title = title;
self.tasks = ko.computed(function() {
$().SPServices({
operation: "GetListItems",
async: true,
webURL: "/projects/" + self.id,
listName: "Tasks",
CAMLQuery: // query..,
CAMLViewFields: "<ViewFields><FieldRef Name='ID' /></ViewFields>",
completefunc: function (xData, Status) {
$(".tasks-" + id).text($(xData.responseXML).SPFilterNode("z:row").length);
}
});
});
}
I had a look at these sites
Async dependantobservables
async computed observables
Is this the way to go?
Edit:
OK so some more code (have stripped out some stuff here):
function onGetProjectsCompleted(projects) {
var projectViewModel = new ProjectViewModel();
projectViewModel.init(projects);
ko.applyBindings(projectViewModel);
}
function beforeLoadingProjects() {
$(".loadingMessage").show();
}
function initProjectsView() {
ProjectsRepository.getOpenProjects(beforeLoadingProjects, onGetProjectsCompleted);
}
function ProjectViewModel() {
var self = this;
self.openProjects = ko.observableArray();
self.init = function initProjectViewModel(projects) {
$.each(projects, function() {
self.openProjects.push(this);
});
});
};
}
var ProjectsRepository = {
getOpenProjects: function (beforeComplete, onComplete) {
var options = {
operation: "GetListItems",
completefunc: function(xData, status) {
var projects = new Array();
$(xData.responseXML).SPFilterNode("z:row").each(function() {
var item = $(this);
projects.push(new ProjectModel(
item.attr("ows_Title"),
item.attr("ows_ProjectID")
));
});
onComplete(projects);
}
};
beforeComplete();
$().SPServices(options);
}
};
Please note, I don't want to list the tasks for each project, I just want to have a property with the Number of tasks for each project, i.e. no array.
Thanks again.

I can't tell how you are trying to use the class (Model) above, but #anders is correct: your model should not manipulate DOM element directly... That's what Knockout's view binding is for...
Try this (disclosure: I did not test this in a live environment):
Model:
function ProjectModel(title, id) {
var self = this;
self.id = id;
self.title = title;
self.tasks = ko.observable(0);
$().SPServices({
operation: "GetListItems",
async: true,
webURL: "/projects/" + self.id,
listName: "Tasks",
CAMLQuery: // query..,
CAMLViewFields: "<ViewFields><FieldRef Name='ID' /></ViewFields>",
completefunc: function (xData, Status) {
if (Status !== "success") {
alert("failed to get tasks! Ajax call error.");
return;
}
self.tasks(
$(xData.responseXML).SPFilterNode("rs:data").attr('ItemCount')
);
}
});
}
View:
<div id="mytemplate">
<h2>
<span data-bind="text: title"></span> Project has
<span data-bind="text: tasks"></span> Tasks
</h2>
</div>
Controller/Binding:
ko.applyBindings(
new ProjectModel("your project title", "your project ID"),
document.getElementById("mytemplate")
);
Try to use the above and see if you get output... The number of tasks will be initially zero, but if your tasks list is valid and tasks are there, then it will update itself once the query completes.

I haven't used SPServices before, but I would start by separating the model from the ajax calls by creating a service object. then pass in the observable to be filled once the
Something like
var ajaxServices = (function(){
return {
getProjectTasks: getProjectTasks
};
function getProjectTasks(projectId, tasksObservable){
$().SPServices({
operation: "GetListItems",
async: true,
webURL: "/projects/" + projectId,
listName: "Tasks",
CAMLQuery: // query..,
CAMLViewFields: "<ViewFields><FieldRef Name='ID' /></ViewFields>",
completefunc: function (xData, Status) {
tasksObservable(xData);
});
}
})();
function ProjectModel(title, id) {
var self = this;
self.id = id;
self.title = title;
self.tasks = ko.observableArray([]);
self.getTasks = function (){
ajaxServices.getProjectsTasks(sif.id, self.tasks);
};
}

Related

Using Meteor.wrapAsync to wrap a callback inside a method

This Meteor code is giving the error:
Error: Meteor code must always run within a Fiber. Try wrapping callbacks that you pass to non-Meteor libraries with Meteor.bindEnvironment.
I tried Meteor.bindEnvironment for no avail and want to try Meteor.wrapAsync. I could not figure it out from the docs. Could some one please help me with the syntax? thx
Meteor.methods({
'createTransaction':
function (nonceFromTheClient, Payment) {
let user = Meteor.user();
gateway.transaction.sale(
{
arg_object
},
function (err, success) {
if (!err) {
//do stuff here
}
}
);
}
});
Wrap in Meteor.wrapAsync
Meteor.methods({
'createTransaction':
function (nonceFromTheClient, Payment) {
this.unblock();
let user = Meteor.user();
var sale = Meteor.wrapAsync(gateway.transaction.sale);
var res = sale({arg_object});
future.return(res);
return future.wait();
}
});
Or try wrapping it in Fiber
var Fiber = Npm.require('fibers');
Meteor.methods({
'createTransaction': function (nonceFromTheClient, Payment) {
Fiber(function() {
let user = Meteor.user();
gateway.transaction.sale(
{
arg_object
},
function (err, success) {
if (!err) {
//do stuff here
}
}
);
}).run()
}
});
Update: Here's how I handle stripe with Async.runSync and Meteor.bindEnvironment
var stripe = require("stripe")(Meteor.settings.private.StripeKeys.secretKey);
Meteor.methods({
'stripeToken': function() {
this.unblock();
var future = new Future();
var encrypted = CryptoJS.AES.encrypt(Meteor.userId(), userIdEncryptionToken);
future.return(encrypted.toString());
return future.wait();
},
'stripePayment': function(token) {
var userId = Meteor.userId();
var totalPrice = 0;
//calculate total price from collection
totalPrice = Math.ceil(totalPrice * 100) / 100;
userEmail = Meteor.users.findOne({
'_id': userId
}).emails[0].address;
// Create a charge: this will charge the user's card
var now = new Date();
Async.runSync(function(done) {
var charge = stripe.charges.create({
amount: Math.ceil(totalPrice * 100), // Amount in cents // coverting dollars to cents
currency: "usd",
source: token,
receipt_email: userEmail,
description: "Charging"
}, Meteor.bindEnvironment(function(err, charge) {
if (err) {
//handle errors with a switch case for different errors
done();
} else {
//handle res, update order
}
}));
}); // Async.runSync
},
});

Create a universal helper variable

Is there a way to create a variable at the top of template helpers to remove duplication.
In this particular situation I'm using var candidate = FlowRouter.getParam('id'); and I have to create the variable in each helper. I assume there is a better way.
professionalOverview: function() {
var candidate = FlowRouter.getParam('id');
return ProfessionalOverview.findOne({ candidateUserId: candidate });
},
candidateImg: function() {
var candidateUserId = FlowRouter.getParam('id');
return Files.findOne({ userId: candidateUserId });
},
EDIT
Template.talentProfileNew.onCreated(function() {
var self = this;
self.autorun(function(){
this.candidateUserId = new ReactiveVar(FlowRouter.getParam('id'));
}
});
Template.talentProfileNew.helpers({
candidate: function() {
console.log(Template.instance().candidateUserId.get());
return Meteor.users.findOne({_id: Template.instance().candidateUserId.get()});
}
});
you could read it once in onCreated() and put it in a reactive var. e.g.
Template.Foo.onCreated(function() {
this.candidateUserId = new ReactiveVar(FlowRouter.getParam('id'));
});
Template.Foo.helpers({
candidateImg() {
return ProfessionalOverview.findOne({ userId: Template.instance().candidateUserId.get()});
}
});

MeteorJS: Collection.find fires multiple times instead of once

I have an app that when you select an industry from a drop down list a collection is updated where the attribute equals the selected industry.
JavaScript:
Template.selector.events({
'click div.select-block ul.dropdown-menu li': function(e) {
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#industryPicker option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('currentIndustryOnet');
if(val != oldVal) {
Session.set('jobsLoaded', false);
Session.set('currentIndustryOnet', val);
Meteor.call('countByOnet', val, function(error, results){
if(results > 0) {
Session.set('jobsLoaded', true);
} else {
getJobsByIndustry(val);
}
});
}
}
});
var getJobsByIndustry = function(onet) {
if(typeof(onet) === "undefined")
alert("Must include an Onet code");
var params = "onet=" + onet + "&cn=100&rs=1&re=500";
return getJobs(params, onet);
}
var getJobs = function(params, onet) {
Meteor.call('retrieveJobs', params, function(error, results){
$('job', results.content).each(function(){
var jvid = $(this).find('jvid').text();
var job = Jobs.findOne({jvid: jvid});
if(!job) {
options = {}
options.title = $(this).find('title').text();
options.company = $(this).find('company').text();
options.address = $(this).find('location').text();
options.jvid = jvid;
options.onet = onet;
options.url = $(this).find('url').text();
options.dateacquired = $(this).find('dateacquired').text();
var id = createJob(options);
console.log("Job Created: " + id);
}
});
Session.set('jobsLoaded', true);
});
}
Template.list.events({
'click div.select-block ul.dropdown-menu li': function(e){
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#perPage option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('perPage');
if(val != oldVal) {
Session.set('perPage', val);
Pagination.perPage(val);
}
}
});
Template.list.jobs = function() {
var jobs;
if(Session.get('currentIndustryOnet')) {
jobs = Jobs.find({onet: Session.get('currentIndustryOnet')}).fetch();
var addresses = _.chain(jobs)
.countBy('address')
.pairs()
.sortBy(function(j) {return -j[1];})
.map(function(j) {return j[0];})
.first(100)
.value();
gmaps.clearMap();
$.each(_.uniq(addresses), function(k, v){
var addr = v.split(', ');
Meteor.call('getCity', addr[0].toUpperCase(), addr[1], function(error, city){
if(city) {
var opts = {};
opts.lng = city.loc[1];
opts.lat = city.loc[0];
opts.population = city.pop;
gmaps.addMarker(opts);
}
});
})
return Pagination.collection(jobs);
} else {
jobs = Jobs.find()
Session.set('jobCount', jobs.count());
return Pagination.collection(jobs.fetch());
}
}
In Template.list.jobs if you console.log(addresses), it is called 4 different times. The browser console looks like this:
(2) 100
(2) 100
Any reason why this would fire multiple times?
As #musically_ut said it might be because of your session data.
Basically you must make the difference between reactive datasources and non reactive datasources.
Non reactive are standard javascript, nothing fancy.
The reactive ones however are monitored by Meteor and when one is updated (insert, update, delete, you name it), Meteor is going to execute again all parts which uses this datasource. Default reactive datasources are: collections and sessions. You can also create yours.
So when you update your session attribute, it is going to execute again all helper's methods which are using this datasource.
About the rendering, pages were rendered again in Meteor < 0.8, now with Blaze it is not the case anymore.
Here is a quick example for a better understanding:
The template first
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>{{getSession}}</h1>
<h1>{{getNonReactiveSession}}</h1>
<h1>{{getCollection}}</h1>
<input type="button" name="session" value="Session" />
<input type="button" name="collection" value="Collection" />
</template>
And the client code
if (Meteor.isClient) {
CollectionWhatever = new Meteor.Collection;
Template.hello.events({
'click input[name="session"]': function () {
Session.set('date', new Date());
},
'click input[name="collection"]': function () {
CollectionWhatever.insert({});
}
});
Template.hello.getSession = function () {
console.log('getSession');
return Session.get('date');
};
Template.hello.getNonReactiveSession = function () {
console.log('getNonReactiveSession');
var sessionVal = null;
new Deps.nonreactive(function () {
sessionVal = Session.get('date');
});
return sessionVal;
};
Template.hello.getCollection = function () {
console.log('getCollection');
return CollectionWhatever.find().count();
};
Template.hello.rendered = function () {
console.log('rendered');
}
}
If you click on a button it is going to update a datasource and the helper method which is using this datasource will be executed again.
Except for the non reactive session, with Deps.nonreactive you can make Meteor ignore the updates.
Do not hesitate to add logs to your app!
You can read:
Reactivity
Dependencies

Firebase on() does not return anything

I have this piece of code using on() to get data from Firebase, inside on() I create object which I want to send out of function for future use - using return, but it seems it doesn't return anything.
So question is how can I make it right?
postsRef.on('value', function(snapshot) {
if (snapshot.val() === null) {
var allPosts = false,
numberOfPosts = 0;
}
else {
var allPosts = snapshot.val(),
numberOfPosts = Object.size(allPosts);
}
var postsData = {
content: allPosts,
count: numberOfPosts
};
return postsData;
});
The callback function is called asynchronously (some time in the future). So by the time it is invoked, postsRef.on(...) has already returned and any code immediately after it will have run.
For example, this might be tempting, but would not work:
var postsData;
postsRef.on('value', function(snapshot) {
postsData = snapshot.val();
});
console.log(postsData); // postsData hasn't been set yet!
So there are a few different ways to tackle this. The best answer will depend on preference and code structure:
Move the logic accessing postsData into the callback
postsRef.on('value', function(snapshot) {
postsData = snapshot.val();
console.log(postsData);
});
Call another function when the callback is invoked
function logResults(postsData) {
console.log(postsData);
}
postsRef.on('value', function(snapshot) {
logResults(snapshot.val());
});
Trigger an event
function Observable() {
this.listeners = [];
}
Observable.prototype = {
monitorValue: function( postsRef ) {
var self = this;
postsRef.on('value', function(snapshot) {
self._notifyListeners(postsRef);
});
},
listen: function( callback ) {
this.listeners.push(callback);
},
_notifyListeners: function(data) {
this.listeners.forEach(function(cb) {
cb(data);
}
}
};
function logEvent( data ) {
console.log(data);
}
var observable = new Observable();
observable.listen( logEvent );
observable.monitorValue( /* postsRef goes here */ );

knockout.js with binding

This is probably a really basic question but in the following view model I am populating self.userData from an ajax call. I want to display this information in the UI and I figured the 'with' binding is the way to go.. however since self.userData is empty until the ajax function is called I get an error from the with binding
HTML
<div data-bind="with: userData">
<div data-bind="text: userId"></div>
...
</div>
Model
var viewModel = function () {
var self = this;
self.userData = {};
self.login = function(data) {
var postData = { ..trimmed out.. };
$.ajax({
type: "POST",
url: "myService",
data: postData
}).done(function (data, status) {
self.userData = ko.mapping.fromJS(data, userData);
console.log(self.userData);
}).fail(function (data, status) {
alert('Could Not Login');
});
}
};
ko.applyBindings(new viewModel());
Initialize userData with an empty observable, an then set it with the object created by the the mapping plugin once the call return. I.e. change
self.userData = {};
with
self.userDara = ko.observable();
and change
self.userData = ko.mapping.fromJS(data, userData);
with
self.userData(ko.mapping.fromJS(data,userData));
You need to initialize userData as an empty observable (single object from ajax call) or observable array (multiple objects) first:
self.userData = ko.observableArray([]);
or
self.userData = ko.observable();

Resources