I'm attempting to implement an asynchronous computed observable as show here.
I can do it successfully for one ajax call. The challenge I have at the moment is how to perform various ajax calls in a loop building an array asynchronously and then returning the array to my computed observable array using jQuery promises.
Basically the HTML form works in the following way:
This a student course form.
For each row, users type the person number and on another column they'll type a list of course ids separated by commas. Eg 100, 200, 300.
The purpose of the computed observable is to store an array
containing course details for the courses entered in step 2.
The details are obtained by firing ajax calls for each course and storing HTTP response in the array.
I don't want users to wait for the result, thus the reason to implement an async computed observable.
My problem: I'm having problem returning the value of the final array to the observable. It's always undefined. The ajax calls work fine but perhaps I'm still not handling the promises correctly.
Here's the code for my class:
function asyncComputed(evaluator, owner) {
var result = ko.observable(), currentDeferred;
result.inProgress = ko.observable(false); // Track whether we're waiting for a result
ko.computed(function () {
// Abort any in-flight evaluation to ensure we only notify with the latest value
if (currentDeferred) { currentDeferred.reject(); }
var evaluatorResult = evaluator.call(owner);
// Cope with both asynchronous and synchronous values
if (evaluatorResult && (typeof evaluatorResult.done == "function")) { // Async
result.inProgress(true);
currentDeferred = $.Deferred().done(function (data) {
result.inProgress(false);
result(data);
});
evaluatorResult.done(currentDeferred.resolve);
} else // Sync
result(evaluatorResult);
});
return result;
}
function personDetails(id, personNumber, courseIds) {
var self = this;
self.id = ko.observable(id);
self.personNumber = ko.observable(personNumber);
self.courseIds = ko.observable(courseIds);
// Computed property to extract PIC details for additional PICs.
// This is computed observable which returns response asynchronously
self.courseDetails = asyncComputed(function () {
var courseIdsArray = self.courseIds().split(",");
var arr = [];
var arr_promises = [];
function getCourseDetails(courseId) {
var dfrd = $.Deferred();
var content = {};
content.searchString = courseId;
var url = 'MyURL';
return $.ajax(url, {
type: 'POST',
dataType: 'json',
data: requestData, // content of requestData is irrelevant. The ajax call works fine.
processdata: true,
cache: false,
async: true,
contentType: "application/json"
}).done(function (data) {
arr.push(new PicDetails(data.GenericIdentifierSearchResult[0]));
}).fail(function () {
alert("Could not retrieve PIC details");
}).then(function () {
dfrd.resolve();
});
}
if (courseIdsArray.length > 0) {
$.each(courseIdsArray, function (index, courseId) {
if (courseId.length > 0) {
arr_promises.push(getCourseDetails(courseId));
}
});
};
$.when.apply($, arr_promises).done(function () {
return arr;
})
}, this);
}
I think you dont really need a separate api/code for this.
You could just create observables for every input/value that changes on your site, and create a computed observable based on those.
e.g in rough pseudo code
self.id = ko.observable(id);
self.personNumber = ko.observable(personNumber);
self.courseIds = ko.observable(courseIds);
self.courseDetailsArray = ko.observableArray([]);
self.courseDetails = ko.computed(function() {
//computed the course details based on other observables
//whenever user types in more course ids, start loading them
$.get( yoururl, {self.courseIds and self.id}).success(data) {
when finished async loading, parse the data and push the new course details into final array
self.courseDetailsArray.push( your loaded and parsed data );
//since courseDetailsArray is observableArray, you can have further computed observables using and re-formatting it.
}
});
I have something a bit different from your approach, but you can build something like an asyncComputed out of it if you prefer:
make a simple observable that will hold the result
make a dictionary of promises that you'll basically keep in sync with the array of course ids
when the array of course ids change, add / remove from the dictionary of promises
wrap all your promises in a when (like you're doing) and set the result when they're all done
Basic idea:
var results = ko.observable([]);
var loadingPromises = {};
var watcher = ko.computed(function () {
var ids = ko.unwrap(listOfIds);
if (ids && ids.length) {
ids.forEach(function (id) {
if (!loadingPromises.hasOwnProperty(id)) {
loadingPromises[id] = $.get(url, {...id...});
}
});
var stillApplicablePromises = {};
var promises = []; // we could delete from loadingPromises but v8 optimizes delete poorly
Object.getOwnPropertyNames(loadingPromises).forEach(function (id) {
if (ids.indexOf(id) >= 0) {
stillApplicablePromises[id] = loadingPromises[id];
promises.push(loadingPromises[id]);
}
});
loadingPromises = stillApplicablePromises;
$.when.apply(this, promises).then(function () {
// process arguments here however you like, they're the responses to your promises
results(arguments);
});
} else {
loadingPromises = {};
results([]);
}
}, this);
This is the file (that may change) where you can see this "in real life": https://github.com/wikimedia/analytics-dashiki/blob/master/src/components/wikimetrics-visualizer/wikimetrics-visualizer.js
And the basic fiddle: http://jsfiddle.net/xtsekb20/1/
Related
I'm trying to call an API that takes parameters start and count, like this:
function handleSuccess() {
if (!!response.data) {
return (response.data);
} else {
return q.reject(response.data);
}
}
function handleError() {
// do some handling
}
function getData(url, sortBy) {
var count = 10;
var start = 1;
var request = http({
cache: true,
method: "GET",
url: url,
params: {
sortBy: sortBy,
sortOrder: "ASC",
count: count, // e.g. 10
start: start // e.g. 1
}
});
return (request.then(handleSuccess, handleError));
}
The JSON response from the API could contain a "next" link that would give me the URL to call to get the next set of data if there exists more...this is how the pagination works.
What's the best way to do this and concatenate all the data returned into one JSON response?
Assuming some part of the data response is an array, then simply use normal array concat() to combine it with previous pages of data in your handleSuccess() callback.
I've found that a service-oriented way is most useful when trying to get paged data from the same endpoint as it is easy to share services and objects between controllers and directives.
First, I would set up the service layer of your application so that all objects being requested have common, generic methods like so (I highly recommend you use ngResource or RESTAngular or something like that):
angular.module('myModule')
.factory('ApiObject', function($http, $q) {
ApiObject = function ApiObject(attributes) {
angular.extend(this, attributes);
};
ApiObject.query = function(url, parameters) {
var deferred = $q.defer();
$http.get(url, {params: parameters}).then(function(data) {
var results = [];
angular.forEach(data, function(apiObject) {
results.push(new ApiObject(apiObject));
});
deferred.resolve(results);
}, function(error) {
// Do error stuff
deferred.reject(error);
});
return deferred.promise;
};
return ApiObject;
});
Then set up a service to manage your paging data that accepts your generic services as well as parameters and configuration options. Also allow for events to be triggered within the service (see trigger and on methods) so that you know when new results are fetched. I've also written in a way for the results to be automatically concatenated onto the current result set:
angular.module('myModule')
.factory('SearchService', function() {
SearchService = function SearchService(service, params, config) {
this.searchParams = params || {};
this.config = config || {};
this.service = service;
this.results = [];
this.listeners = {};
};
SearchService.prototype.fetch = function(params) {
var _this = this;
this.service.query().then(function(results) {
if(_this.config.concatResults) {
_this.results = _this.results.concat(results);
// You probably should make sure results are unique at this point as that is a common problem with paging a changing API
} else {
_this.results = results;
}
_this.trigger('searchSuccess', _this.results);
});
};
SearchService.prototype.on = function(event, listener) {
(this.listeners[event] = (this.listeners[event] || [])).push(listener);
};
SearchService.prototype.trigger = function(event, payload) {
angular.forEach(this.listeners[event], function(listener) {
listener(payload);
});
};
SearchService.prototype.isLastPage = function() {
//logic here to determine last page
};
SearchService.prototype.nextPage = function() {
if(this.isLastPage()) {
return;
}
if(this.searchParams.page) {
this.searchParams.page++;
} else {
this.searchParams.page = 2;
}
this.fetch();
};
// Write more methods for previousPage, lastPage, firstPage, goToPage... etc.
return SearchService;
});
Then in your controller, you're going to want to instantiate the search service with some default parameters and configuration and then fetch the first page:
angular.module('myModule')
.controller('MyCtrl', function($scope, ApiObject, SearchService) {
$scope.searchService = new SearchService(ApiObject, {page: 1}, {concatResults: true});
$scope.searchService.on('searchSuccess', function(results) {
// Do something with results if you wish, but they'll already be stored in $scope.searchService
});
// Get the first page of data
$scope.searchService.fetch();
});
This is obviously a rough cut with a lot of room for improvement, but I hope this will be a good jumping off point to get you pointed in some sort of angular-style direction. In my experience, this is the best way to abstract out paging logic from the data/request layer in your services.
I've got a factory function that won't return a variable I'm trying to set in my controller. I don't get an error though, just the variable won't get set to what it's suppose to.
spApp.factory('SiteService', function ($q){
var rootUrl = window.location.protocol + "//" + window.location.hostname;
var siteMap;
//returns object containing info about all sites within collection
var getSiteMap = function () {
siteMap = {};
var promise = $().SPServices({
operation: "GetAllSubWebCollection",
async: true
});
promise.then(
function (response){
map = {}; //init the map
var web = $(response).find("Web").map(function () {
return $(this).attr('Url');
});
var webTitle = $(response).find("Web").map(function () {
return $(this).attr('Title');
});
// create map
for (var i = 0; i < web.length; i++) {
var item = web[i],
title = webTitle[i],
parts = item.split('/'),
domain = parts.splice(0, 3).join('/'),
current;
if (!map[domain]) map[domain] = {url:domain, title:title ,children:{}};
current = map[domain].children;
for (var index in parts) {
var part = parts[index];
if (!current[part]) {
current[part] = {url:domain+'/'+parts.slice(0,index+1).join('/'), title:title, children:{}};
}
current = current[part].children;
}
}
siteMap = map;
}, function(reason){
alert('FAILED:' + reason);
})
console.log(siteMap);
return siteMap;
}
return{
getSiteMap:getSiteMap
}
});
Try chaining your promises like this:
var getSiteMap = function () {
siteMap = {};
var promise = $().SPServices({
operation: "GetAllSubWebCollection",
async: true
});
return promise.then(function(response){ //return your promise
// all you code
siteMap = map;
return siteMap; //return a value to another .then in the chain
});
}
Use it like this:
SiteService.getSiteMap().then(function(siteMap){
});
The issue you have is that you are working with promises. When you put your console.log outside your then() function, you are logging the variable before it has actually been resolved.
If you put your console.log inside your then() function (after sitemap is assigned), it should show the correct value, but you still won't be able to access it reliably.
I think the simplest way for you to access the siteMap value after it has been populated with data is to pass in a callback function. Eg:
var getSiteMap = function (_callback) {
siteMap = {};
$().SPServices({
operation: "GetAllSubWebCollection",
async: true
}).then(function(response){
// Process the data and set siteMap
// ...
siteMap = map;
// now pass siteMap to the callback
_callback(siteMap);
});
You would then use this in your controller like so:
SiteService.getSiteMap(function(sitemap){
// Do something with your sitemap here
});
Now while this will work, it is just one quick example, and not necessarily the best way. If you don't like callbacks, you could create a second promise that resolves only when siteMap is assigned. Also depending on your use case for getSiteMap(), you may want to cache the value, otherwise the request will be called every time.
Have been looking into getting callbacks for ajax() responses using $.when I'm still unsure how this works fully but this is what I would like the below to do.
When a user adds a town and country per line, it goes to the url in the .ajax() I get a response and it pushes the array to be usable outside of the .each() loop.
At the moment you will see inside here at jsbin that when the button is pressed firstly the response in console.log is [] then when I press it again the addresses show up. then a 3rd press will add the addresses again which shouldn't happen.
jQuery
var addresses,town;
var arrayLocation = [];
$('button').click(function(){
addresses = function() {
deferred = new $.Deferred();
var arrayOfLines = $('#gps').val().split('\n');
$.each(arrayOfLines, function(index, item) {
town = item.split(',');
$.ajax({
url: 'http://maps.googleapis.com/maps/api/geocode/json?address='+town[0]+'&sensor=false',
dataType: 'json',
success: function (data) {
add = data.results[0].address_components[0].long_name;
lat = data.results[0].geometry.location.lat;
lng = data.results[0].geometry.location.lng;
arrayLocation.push("['"+add+"', "+lat+", "+lng+"]");
console.log("['"+add+"', "+lat+", "+lng+"]");
}
});
});
return arrayLocation;
};
$.when(addresses()).then(function(arrayLocation){
console.log(arrayLocation);
});
});
You are not using $.when correctly. The main problem is that the addresses function returns a bare array. It's true that this array will be populated in the future when an async operation (the set of AJAX calls) completes, but from the point of view of addresses's caller this is impossible to know. Therefore the caller has absolutely no chance of reacting to the operation being completed.
What you would normally do is return the return value of $.ajax to the caller, somewhat like this:
addresses = function() {
return $.ajax({ ... });
};
The caller could then do
$.when(addresses()).then(function(result) { ... });
In this particular example however this is not directly possible because there are multiple AJAX calls being made, so you need some way of "combining" all of them into one package. There are multiple ways to do this, so there is also a matter of what you prefer here.
One solution would be to use an array of AJAX promises:
$('button').click(function(){
var arrayLocation = [];
addresses = function() {
var promises = [];
var arrayOfLines = $('#gps').val().split('\n');
$.each(arrayOfLines, function(index, item) {
town = item.split(',');
promises.push($.ajax({
url: 'http://maps.googleapis.com/...',
dataType: 'json',
success: function (data) {
add = data.results[0].address_components[0].long_name;
lat = data.results[0].geometry.location.lat;
lng = data.results[0].geometry.location.lng;
arrayLocation.push("['"+add+"', "+lat+", "+lng+"]");
console.log("['"+add+"', "+lat+", "+lng+"]");
}
}));
});
return promises;
};
$.when.apply($, addresses()).then(function(){
console.log(arrayLocation);
});
});
There are a couple points of note here:
Returning an array of independent promises means that you cannot feed them to $.when directly because that function is designed to accept multiple individual promises instead of an array; you need to use apply to compensate.
I have moved the declaration of arrayLocation inside the click event handler so that it gets reset each time you click the button. The problem of results being added again after each click was due to this array not being reset.
The final handler does not accept any arguments. That's because the arguments that will be passed are the jqXHR objects representing the individual AJAX requests, which is not really useful. Instead of this it captures arrayLocation by closure, since you know independently that the results will be stored there.
Slightly different approach without a shared global variables
$('button').click(function () {
var addresses = function () {
var arrayOfLines = $('#gps').val().split('\n'),
arrayLocation = [];
$.each(arrayOfLines, function (index, item) {
var town = item.split(',');
var xhr = $.ajax({
url: 'http://maps.googleapis.com/maps/api/geocode/json?address=' + $.trim(town[0]) + '&sensor=false',
dataType: 'json'
});
arrayLocation.push(xhr);
});
return $.when.apply($, arrayLocation).then(function () {
return $.map(arguments, function (args) {
if (!$.isArray(args[0].results) || args[0].results.length == 0) {
return undefined;
}
var data = args[0].results[0];
var location = data.geometry.location;
var add = data.address_components[0].long_name;
var lat = location.lat;
var lng = lng;
return "['" + add + "', " + lat + ", " + lng + "]";
});
});
return arrayLocation;
};
addresses().done(function (arrayLocation) {
console.log(arrayLocation)
})
});
Demo: Fiddle
I'm working on creating a Users collection with the ability to then grab single users inside. This will be used to match from another system, so my desire is to load the users once, and then be able to fine/match later. However, I'm having a problem accessing the outer users collection from an inner method.
function Users(){
var allUsers;
this.getUsers = function () {
// ajax to that Jasmine behaves
$.ajax({
url: '../app/data/jira_users.json',
async: false,
dataType: 'json',
success: function(data) {
allUsers = data;
}
});
return allUsers;
};
this.SingleUser = function (name) {
var rate = 0.0;
var position;
this.getRate = function () {
if(position === undefined){
console.log('>>info: getting user position to then find rate');
this.getPosition();
}
$.ajax({
url: '../app/data/rates.json',
async: false,
dataType: 'json',
success: function(data) {
rate = data[position];
}
});
return rate;
};
this.getPosition = function () {
console.log(allUsers);
//position = allUsers[name];
return position;
};
//set name prop for use later I guess.
this.name = name;
};
}
and the test that's starting all of this:
it("get single user's position", function(){
var users = new Users();
var someone = new users.SingleUser('bgrimes');
var position = someone.getPosition();
expect(position).not.toBeUndefined();
expect(position).toEqual('mgr');
});
The getPosition method is the issue (which might be obvious) as allUsers is always undefined. What I have here is yet another attempt, I've tried a few ways. I think the problem is how the Users.getUsers is being called to start with, but I'm also unsure if I'm using the outer and inner vars is correct.
Though the others are correct in that this won't work as you have it typed out, I see the use case is a jasmine test case. So, there is a way to make your test succeed. And by doing something like the following you remove the need to actually be running any kind of server to do your test.
var dataThatYouWouldExpectFromServer = {
bgrimes: {
username: 'bgrimes',
show: 'chuck',
position: 'mgr'
}
};
it("get single user's position", function(){
var users = new Users();
spyOn($, 'ajax').andCallFake(function (ajaxOptions) {
ajaxOptions.success(dataThatYouWouldExpectFromServer);
});
users.getUsers();
var someone = new users.SingleUser('bgrimes');
var position = someone.getPosition();
expect(position).not.toBeUndefined();
expect(position).toEqual('mgr');
});
This will make the ajax call return whatever it is that you want it to return, which also allows you to mock out tests for failures, unexpected data, etc. You can set 'dataThatYouWouldExpectFromServer' to anything you want at any time.. which can help with cases where you want to test out a few different results but don't want a JSON file for each result.
Sorta-edit - this would fix the test case, but probably not the code. My recommendation is that any time you rely on an ajax call return, make sure the method you are calling has a 'callback' argument. For example:
var users = new Users();
users.getUsers(function () {
//continue doing stuff
});
You can nest them, or you can (preferably) create the callbacks and then use them as arguments for eachother.
var users = new Users(), currentUser;
var showUserRate = function () {
//show his rate
//this won't require a callback because we know it's loaded.
var rate = currentUser.getRate();
}
var usersLoaded = function () {
//going to load up the user 'bgrimes'
currentUser = new users.SingleUser('bgrimes');
currentUser.getRate(showUserRate);
}
users.getUsers(usersLoaded);
your approach to fill the data in allUsers is flawed
the ajax call in jquery is async so every call to users.getAllUsers would be returned with nothing and when later the success function of the jquery ajax is called then allUsers would get filled
this.getUsers() won't work. Its returning of allUsers is independent from the ajax request that fetches the data, because, well, the ajax is asynchronous. Same with getRate().
You'll have to use a callback approach, where you call getUsers() with a callback reference, and when the ajax request completes, it passes the data to the callback function.
Something like:
this.getUsers = function (callback) {
// ajax to that Jasmine behaves
$.ajax({
url: '../app/data/jira_users.json',
async: false,
dataType: 'json',
success: function(data) {
callback(data);
}
});
};
And the call would be along the lines of:
var user_data = null;
Users.getUsers(function(data) {
user_data = data;
});
I have a the following java script object
function eventTypeObj() {
allEventTypes = [];
// When the object is created go and get all the event types that can be included in journey or clusters.
$.ajax({
url: "/ATOMWebService.svc/GetDisplayEventTypes",
dataType: "json",
success: function(result) {
allEventTypes = eval("(" + result.d + ")");
}
});
// Returns a list of all the event type IDS.
this.getEventTypeIds = function() {
var eventTypeIDs = [];
for (var i = 0; i < allEventTypes.length; i++) {
eventTypeIDs.push(allEventTypes[i].Id);
}
return eventTypeIDs;
};
}
I was wondering if there is a way stop some one calling the eventTypeObj.getEventTypeIds(); before the ajax call in the constructor has succeeded, and there is no data in the allEventTypes array?
Something like this would be way better (im not guaranteeing this is 100% working, but the concept is sound):
function eventTypeObj() {
this.allEventTypes = [];
this.hasLoadedEventTypes = false;
var loadEventTypes = function(cb) {
$.ajax({
url: "/ATOMWebService.svc/GetDisplayEventTypes",
dataType: "json",
success: function(result) {
this.allEventTypes = eval("(" + result.d + ")");
this.hasLoadedEventTypes = true;
cb();
}
});
};
this.getEventTypeIds = function(updateEventTypes, callback) {
var _getEventTypeIds = function() {
var eventTypeIDs = [];
for (var i = 0; i < this.allEventTypes.length; i++) {
eventTypeIDs.push(this.allEventTypes[i].Id);
}
return eventTypeIDs;
};
if (!this.hasLoadedEventTypes || updateEventTypes) {
loadEventTypes(function(){ callback(_getEventTypeIds()); });
}
else callback(_getEventTypeIds());
};
}
Example usage:
var eto = new eventTypeObj();
eto.getEventTypeIds(false, function(eventTypeIdArray) {
// do stuff with the id array
});
/*
somewhere later on you want to get an updated eventTypeId array
in case the event types have changed.
*/
eto.getEventTypeIds(true, function(eventTypeIdArray) {
// do stuff with the updated ids
});
var allowCall = false;
function eventTypeObj() {
allEventTypes = [];
// When the object is created go and get all the event types that can be included in journey or clusters.
$.ajax({
url: "/ATOMWebService.svc/GetDisplayEventTypes",
dataType: "json",
success: function(result) {
allEventTypes = eval("(" + result.d + ")");
allowCall = true;
}
});
// Returns a list of all the event type IDS.
this.getEventTypeIds = function() {
if(!allowCall) return; // or pop up a message
var eventTypeIDs = [];
for (var i = 0; i < allEventTypes.length; i++) {
eventTypeIDs.push(allEventTypes[i].Id);
}
return eventTypeIDs;
};
}
Or just check if allEventTypes is empty or not.
There is no way to prevent someone from calling it too soon. What would you want to have happen if they call it too soon?
It looks like your code now currently returns an empty array if allEventTypes hasn't yet been filled in. You can decide whether the empty array is the right result or if you should throw an exception when it's called too early to make it absolutely clear to the caller that the data is not yet available.
You could provide some helper code for people who need that information, but it might not yet be available. For example, you could allow them to register a callback that would get called from the success handler after the data had been filled in. You could allow them to query whether the data is available yet.
If you don't want the responsibility for the timing to be on the callers, then you cannot offer a synchronous way to get this information. Instead, you would only offer a callback mechanism for getting the data. If the data is ready, the callback would get called immediately. If the data is not ready, the callback would get called when the ajax function completes. In either case, the caller would have to process the data in the callback only and getEventTypeIds would not be a normal call to get the data like it is now, but rather a call to register a callback that would be called with the data when was ready. This would relieve the caller from having to know implementation details of when the data was ready, but would force them to use the asynchronous nature of the callback mechanism.
this.getEventTypeIds = function(callback) {
if (allEventTypes.length > 0) {
// data is ready call the callback with the data now
} else {
// store the callback to be called later from the success handler
}
}
You can check if the eventType array is empty, right?
if(allEventTypes.length == 0)
{
return;
}