Following the suggestions from AngularJS validation and promises, I would like to chain confirmation dialogs and thus validate several steps at once.
Based on data provided by the user, an API call is made to see what all needs to be confirmed by the user.
For each step that needs confirmation, prompt the user and let them decide whether to go to next step.
If any step returns false, the whole chain should return false.
I've been reading a lot about async JS and promises, but I have to admit I am still fairly new to it.
How to properly chain these to get a final true/false for all steps? Note that an API call is needed to determine what all needs to be shown to the user based on provided information, hence fetchSomeData() as first call in the chain.
Any help or suggestions will be greatly appreciated.
fetchSomeData = function() {
var deferred = $q.defer();
api.fetchData(param1, param2, param3)
.then(function(data) {
deferred.resolve(data.content);
}, api.errorHandler);
return deferred.promise;
}
// data = {condition1: false, condition2: true, condition3: true}
// display confirmation dialogs for step 2 and step 3, not step 1
confirmStep1 = function(data) {
if (data.condition1) {
return confirmDialogService.popConfirm('step1').then(function(confirmed) {
return confirmed;
}, function() {
return false;
});
} else {
return $q.when(true);
}
}
confirmStep2 = function(data) {
if (data.condition2) {
return confirmDialogService.popConfirm('step2').then(function(confirmed) {
return confirmed;
}, function() {
return false;
});
} else {
return $q.when(true);
}
}
confirmStep3 = function(data) {
if (data.condition3) {
return confirmDialogService.popConfirm('step3').then(function(confirmed) {
return confirmed;
}, function() {
return false;
});
} else {
return $q.when(true);
}
}
confirmSteps = function() {
return fetchSomeData()
.then(confirmStep1(data))
.then(confirmStep2(data))
.then(confirmStep3(data));
}
confirmSteps().then(function(allConfirmed) {
if (allConfirmed == true) {
doSomething();
} else {
return;
}
});
dfsq started writing an answer but deleted his so with his blessing I'm adding my take on it:
confirmSteps = function() {
return fetchSomeData()
.then(confirmStep1(data))
.then(confirmStep2(data))
.then(confirmStep3(data));
}
This calls functions, it's the same as setTimeout(alert("Hi"),5) you don't want to be calling the functions you want to chain them. Like setTimeout(function(){ alert("Hi"); }, 5);
confirmSteps = function() {
return fetchSomeData()
.then(confirmStep1)
.then(confirmStep2)
.then(confirmStep3);
}
However, that would pass data to the first promise only and the result of the previous promise to the next one, instead you want to pass data to all three, you can do this by neting one level:
confirmSteps = function() {
return fetchSomeData().then(function(data){
var v1, v2;
return confirmStep1(data).then(function(d){
v1 = d;
return confirmStep2(data);
}).then(function(d){
v2 = d;
return confirmStep3(data);
}).then(function(v3){
return v1 && v2 && v3;
})
});
};
This works but it's kind of crufty, instead you can use short circuiting - kind of like how && only evaluates the left hand side if it's falsey. Moreover we can do all error handling in a central location. This would make your code look like.
confirmStep1 = function(data) {
if (data.condition1) return $q.when(true);
return confirmDialogService.popConfirm('step1');
};
confirmStep2 = function(data) {
if (data.condition2) return $q.when(true);
return confirmDialogService.popConfirm('step2');
};
confirmStep3 = function(data) {
if (data.condition3) return $q.when(true);
return confirmDialogService.popConfirm('step3'):
};
confirmSteps = function() {
var data = fetchSomeData();
return data.then(confirmStep1).then(function(soFar){
if(!soFar) return false;
return data.then(confirmStep2);
}).then(function(soFar){
if(!soFar) return false;
return data.then(confirmStep3);
}).catch(function(){ return false; });
};
As an extra tip this:
fetchSomeData = function() {
var deferred = $q.defer();
api.fetchData(param1, param2, param3)
.then(function(data) {
deferred.resolve(data.content);
}, api.errorHandler);
return deferred.promise;
};
Can simply become:
fetchSomeData = function() {
return api.fetchData(param1, param2, param3).then(function(data) {
return data.content;
}, api.errorHandler);
};
Related
I've a bunch of functions which are nested due to top level function is a ajax request.
So i want to return a value instead of a promise in nested child function.
Parent
let getUserPermissions = function(id) {
let deferred = $q.defer();
let promise = accessRequestService.getPermissions(id);
promise.then(function(data) {
deferred.resolve(data);
}, function(err) {
deferred.reject(err);
})
return deferred.promise;
}
Child 1
$rootScope.userInit = function() {
return getUserPermissions(vzid)
.then(function(data) {
//Some code here
return data;
})
}
Child 2
let checkAuthorize = function(toState) {
return $rootScope.userInit().then(
function(data) {
//some code here
return data;
});
}
Level 3
checkAuthorize(toState).then( function(val){
$rootScope.isAuthorized = val;
if ($rootScope.isAuthorized == true) {
$log.info('is Authorized')
} else {
$log.info('is not Authorized');
throw new AuthorizationError()
}
})
At Level 3 we are still working with a promise. Can child 2 return a value instead of promise.
Expectation # Level 3
$rootScope.isAuthorized = checkAuthorize(toState);
if ($rootScope.isAuthorized == true) {
$log.info('is Authorized')
} else {
$log.info('is not Authorized');
throw new AuthorizationError()
}
The hard truth is: you can't, unless you want spaghetti code all around.
The best solution would be to use something like ui-router's resolve, getting all the permissions needed before the page is shown to the user. Then, you could use them on your controllers without any asynchronous calls.
You can use for it async/await construction. And use Babel for support old browsers.
Async
Await
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
var x = await resolveAfter2Seconds(10);
console.log(x); // 10
console.log('done');
}
f1();
Yes, this type of thing is possible, but it will change the behavior. You'll probably want to keep userInit, but you also add a userInitValue variable and initialize it as follows:
let userInitValue = null;
let userInit = function() {
return getUserPermissions()
.then(function(data) {
userInitValue = data;
return data;
})
}
So now userInitValue will start as null and then later be initialized to the relevant data.
function isKnownAuthorized(toDoSomething) {
// If we don't know whether the user is authorized
// because we are still waiting for the server to tell us
// then return false and disallow access for now
if(!userInitValue) return false;
// Otherwise return the truth
// (as of when we got the server response)
return userInitValue.isAuthorized(toDoSomething);
}
Note again the change in behavior. The price of getting an instant response, perhaps before the server gives you the data, is that the instant response could be wrong. So don't use this in a one-time :: expression in AngularJs.
Based on what you're hoping to achieve in Level 3, I'm guessing this function is going to be called multiple times with the same input. In this case, what I would do is make the call to the promise if there is not a cached result, and cache the result. This way you don't have to go down the promise chain, although I only count one promise in the code provided. There are multiple handlers on resolve, but only one promise.
You can run your code as if it was synchronous using nsynjs: it will evaluate code step-by-step, and if some function returns promise, it will pause execution, wait until promise is resolved, and assigns resolve result to data property. So, code below will be paused on level 1 until promise is resolved to actual value.
var getUserPermissions = function(id) {
return new Promise(function(resolve, reject) {
setTimeout(function(){
resolve({
id: id,
isAdmin: "yes he is",
})
}, 1000);
});
};
function synchronousCode() {
console.log("start");
var vzid = 35;
var userInit = function() {
return getUserPermissions(vzid).data;
};
var checkAuthorize = function() {
return userInit().isAdmin;
};
var isAuthorized = checkAuthorize();
console.log(isAuthorized);
};
nsynjs.run(synchronousCode, null, function(){
console.log("finish");
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
I'm using $state.transitionTo Method to be called before $stateChangeStart.
var transitionTo = $state.transitionTo;
$state.transitionTo = function(to, toParams, options) {
var from = $state.$current,
fromParams = $state.params;
to = to.name ? to : $state.get(to);
$rootScope.state = {
to: to.self,
toParams: toParams,
from: from.self,
fromParams: fromParams,
options: options
}
if (options.notify && options.notify !== false) {
return $q.reject(new AuthorizationError('Rejecting $state.transitionTo', 'Transition Rejected'));
} else {
return checkAuthorize(to).then(function(auth) {
$rootScope.isAuthorized = auth;
return transitionTo(to, toParams, options)
})
}
}
StateChangeStart
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
$log.info("Route change start from", fromState.url, "to", toState.url);
//event.preventDefault();
if ($rootScope.isAuthorized == true) {
$log.info('is Authorized')
//$state.go($rootScope.toState.name);
} else {
event.preventDefault();
$log.info('is not Authorized');
throw new AuthorizationError('User is not Authorized.', 'NOT_AUTHENTICATED')
}
});
UPDATE:
I decided that using the JS Module Pattern was not "keeping it simple", so I scrapped it and used jQuery's deferred object to return the data I was looking for. What I really needed was to simply load a JSON file and populate an object. I was just trying to be too fancy by incorporating the JS Module Pattern.
Many thanks to #kiramishima for the correct answer.
Below is the finished code:
function getData(){
var url = CONTEXT + "/json/myJsonFile.json";
return $.getJSON(url);
}
getData()
.done(function(data){
myGlobalObj = data;
})
.fail(function(data){
console.log("fetching JSON file failed");
});
I think I'm getting a little too fancy for my own good here. I'm loading a JSON file and trying to return the API via JS module pattern. Problem is that I believe I'm not implementing the promise correctly and I don't know how to fix it.
Here's my JSON:
{
"result": {
"one": {
"first_key":"adda",
"second_key":"beeb",
"third_key":"cffc"
},
"two": {
"first_key":"adda",
"second_key":"beeb",
"third_key":"cffc"
}
}
}
And here's my JS Module implementation:
var data = (function() {
var url = "/json/dummy.json";
var getAllData = function() {
return $.getJSON(url, function(result){});
};
var promise = getAllData(); // the promise
return {
getFirstObjSecondKey:function() {
return promise.success(function(data) {
return data.result.one.second_key;
});
},
getSecondObjThirdKey:function() {
return promise.success(function(data) {
return data.result.two.third_key;
});
},
};
})();
The problem is that "getAllData()" is coming back as undefined and I'm not sure why; that method returns a Promise that I should be able to handle in the "done" function. How far off am I?
Thanks for any helpful input. This is the first time I'm messing with the JS Module Pattern.
I dont know what is your problem, but I test with:
var getAllData = function() {
return $.getJSON('/json/dummy.json', function(result){})
}
getAllData().done(function(data){ console.log(data.result.one.second_key) }) // prints beeb
works fine in that case, but if try this:
var data = (function() {
var url = '/json/dummy.json';
var getAllData = function() {
return $.getJSON(url, function(result){});
};
return {
getFirstObjSecondKey:function() {
getAllData().done(function(data) {
return data.login;
});
},
getSecondObjThirdKey:function() {
getAllData().done(function(data) {
return data.name;
});
},
};
})();
data.getFirstObjSecondKey returns undefined, then can u pass anonymous function:
var data = (function() {
var url = '/json/dummy.json';
var getAllData = function() {
return $.getJSON(url, function(result){});
};
return {
getFirstObjSecondKey:function(callback) {
getAllData().done(function(data) {
callback(data.result.one.second_key);
});
},
getSecondObjThirdKey:function(callback) {
getAllData().done(function(data) {
callback(data.result.two.third_key);
});
},
};
})();
var t;
data.getFirstObjSecondKey(function(data){
//data should contain the object fetched by getJSON
console.log(data); // prints beeb
t = data; // assign t
})
console.log(t) // prints beeb
Other solution, return always the deferred object
kiramishima's answer works, but it mixes callbacks with Promises. If you're using promises, you should try not to mix both styles.
You have to return a Promise from your functions. Remember that promises can be chained, that is, if you return a Promise from the done function, that becomes the new Promise
var data = (function() {
var url = "/json/dummy.json";
var getAllData = function() {
return $.getJSON(url, function(result){});
};
return {
getFirstObjSecondKey:function() {
return getAllData().done(function(data) {
return new Promise(function(resolve, reject){
resolve(data.result.one.second_key);
});
});
},
getSecondObjThirdKey:function() {
return getAllData().done(function(data) {
return new Promise(function(resolve, reject){
resolve(data.result.one.third_key);
});
});
},
};
})();
data.getFirstObjSecondKey().done(function(secondKey) {
console.log('Second key', secondKey);
});
In one of my beforeSave functions, I need to check a number of conditions before responding with success or error.
The problem is, my code seems really messy and sometimes success/error isn't called:
Parse.Cloud.beforeSave("Entry", function(request, response) {
var entry = request.object;
var contest = request.object.get("contest");
entry.get("user").fetch().then(function(fetchedUser) {
contest.fetch().then(function(fetchedContest) {
if ( fetchedUser.get("fundsAvailable") < fetchedContest.get("entryFee") ) {
response.error('Insufficient Funds.');
} else {
fetchedContest.get("timeSlot").fetch().then(function(fetchedTimeSlot) {
var now = new Date();
if (fetchedTimeSlot.get("startDate") < now) {
response.error('This contest has already started.');
} else {
contest.increment("entriesCount");
contest.increment("entriesLimit", 0); //have to do this, otherwise entriesLimit is undefined in save callback (?)
contest.save().then(function(fetchedContest) {
if (contest.get("entriesCount") > contest.get("entriesLimit")) {
response.error('The contest is full.');
} else {
response.success();
}
});
}
});
}
});
});
});
I've been trying to learn promises to tidy this up, and here was my (failed) attempt:
Parse.Cloud.beforeSave("Entry", function(request, response) {
var entry = request.object;
var contest = request.object.get("contest");
entry.get("user").fetch().then(function(fetchedUser) {
contest.fetch().then(function(fetchedContest) {
if ( fetchedUser.get("fundsAvailable") < fetchedContest.get("entryFee") ) {
response.error('Insufficient Funds.');
}
return fetchedContest;
});
}).then(function(result) {
result.get("timeSlot").fetch().then(function(fetchedTimeSlot) {
var now = new Date();
if (fetchedTimeSlot.get("startDate") < now) {
response.error('This contest has already started.');
}
});
}).then(function() {
contest.increment("entriesCount");
contest.increment("entriesLimit", 0);
contest.save().then(function(fetchedContest) {
if (contest.get("entriesCount") > contest.get("entriesLimit")) {
response.error('The contest is full.');
}
});
}).then(function() {
response.success();
}, function(error) {
response.error(error);
});
});
Any help or examples on how to use promises for this would be much appreciated. Clearly I do not fully understand how they work syntactically yet.
I cleaned it up a bit by getting the fetched variables assembled first using a chain of promises. This follows a couple style rules that make it more readable (for me anyway)...
Parse.Cloud.beforeSave("Entry", function(request, response) {
var entry = request.object;
var contest = request.object.get("contest");
var fetchedUser, fetchedContest;
var errorMessage;
entry.get("user").fetch().then(function(result) {
fetchedUser = result;
return contest.fetch();
}).then(function(result) {
fetchedContest = result;
return fetchedContest.get("timeSlot").fetch();
}).then(function(fetchedTimeSlot) {
// now we have all the variables we need to determine validity
var now = new Date();
var hasSufficientFunds = fetchedUser.get("fundsAvailable") >= fetchedContest.get("entryFee");
var contestNotStarted = fetchedTimeSlot.get("startDate") >= now;
if (hasSufficientFunds && contestNotStarted) {
contest.increment("entriesCount");
contest.increment("entriesLimit", 0); //have to do this, otherwise entriesLimit is undefined in save callback (?)
return contest.save();
} else {
errorMessage = (hasSufficientFunds)? 'This contest has already started.' : 'Insufficient Funds.';
return null;
}
}).then(function(result) {
if (!result) {
response.error(errorMessage);
} else {
if (contest.get("entriesCount") > contest.get("entriesLimit")) {
response.error('The contest is full.');
} else {
response.success();
}
}
}, function(error) {
response.error(error);
});
});
Note how we never leave a response.anything() dangling, we always make clear what's flowing to the next promise via a return.
You are doing the rookie mistake of not returning promises inside .then while chaining them, other than that your chain would still continue even when you call response.error(.., simplest way to break chain is throwing an error, your code can be flattened as:
Parse.Cloud.beforeSave("Entry", (request, response) => {
var entry = request.object,
, contest = request.object.get("contest")
, fetchedUser
, fetchedContest;
entry.get("user").fetch()
.then(_fetchedUser => {
fetchedUser = _fetchedUser;
return contest.fetch();
}).then(_fetchedContest => {
fetchedContest = _fetchedContest;
if ( fetchedUser.get("fundsAvailable") < fetchedContest.get("entryFee") )
return Promise.reject('Insufficient Funds.');
return fetchedContest.get("timeSlot").fetch();
}).then(fetchedTimeSlot => {
var now = new Date();
if (fetchedTimeSlot.get("startDate") < now)
return Promise.reject('This contest has already started.');
contest.increment("entriesCount");
contest.increment("entriesLimit", 0); //have to do this, otherwise entriesLimit is undefined in save callback (?)
return contest.save();
}).then(fetchedContest => {
if (contest.get("entriesCount") > contest.get("entriesLimit"))
return Promise.reject('The contest is full.');
response.success();
}).catch(response.error.bind(response));
});
I am trying to write a loop which performs a number of http requests and adds each response to a list.
However, I don't think I am going about it quite the right way.
I think I am not implementing the required promises correctly. The console log after the for loop shows myList array as empty.
Code:
var _myList = []
function getStuff() {
var deferred = $q.defer()
var url = someUrl
$http.get(url).success(function(response) {
if ( response.array.length > 0 ) {
// loop starts here
for ( var i=0; i < response.array.length; i++ ) {
getThing(response.array[i].id);
};
// check the varibale here
console.log(_myList);
deferred.resolve('Finished');
} else {
deferred.resolve('No stuff exists');
};
}).error(function(error) {
deferred.reject(error);
});
return deferred.promise;
};
function getThing(thindId) {
var deferred = $q.defer()
var url = someUrl + thingId;
$http.get(url).success(function(response) {
_myList.push(response);
deferred.resolve(response);
}).error(function(error) {
deferred.reject(error);
});
return deferred.promise;
};
You can simplify your code as follows:
var allThings = response.array.map(function(id){
var singleThingPromise = getThing(id);
//return a single request promise
return singleThingPromise.then(function(){
//a getThing just ended inspect list
console.log(_myList);
})
});
$q.all(allThings).then(function(){
//only resolve when all things where resolved
deferred.resolve('Finished');
}, function(e){
deferred.reject('Something went wrong ' + e);
});
You indeed won't be able to populate _myList array with for-loop like you set up. Instead create an array of promises - one per data item in response.array and return it as inner promise.
function getStuff() {
var url = someUrl;
return $http.get(url).then(function(response) {
if (response.data.array.length > 0) {
return $q.all(response.data.array.map(function(data) {
return getThing(data.id);
}));
} else {
return 'No stuff exists';
}
});
}
function getThing(thindId) {
var url = someUrl + thingId;
return $http.get(url).then(function(response) {
return response.data;
});
}
After that you would use getStuff like this:
getStuff().then(function(myList) {
console.log(myList);
});
I got a question regarding the solr-client module of nodejs. I'm using this module for querying against a solr-index.
The module itself works fine as long as I don't have to wait for finishing of the query and as long I need the result only as a async result.
But currently I cannot find out, how I will be able to await the finishing of a search request and use the result in a sequential way.
I have the follwing method in my manager
SolrManager.prototype.promisedQuery = function(query, callback) {
var solrClient = solr.createClient(this.configuration.cores.page);
var docs = null;
var finished = false;
var deferred = Q.defer();
var request = solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
var records = null;
var promise = deferred.promise;
promise.then(function(result) {
records = result;
}).fail(function(error){
records = error;
});
return records;
};
The problem here is, that I try to wait for the result of the query and use it as return value of "promisedQuery".
I try since days to use this method in a sequential call, also with different additional modules like "wait.for", "q", etc. but nothing seems to work.
The callback function of the solr-client will always be executed after the manager-method has already returned. Also the promise-methods will be even called after the return from the manager-method.
Can someone help me out on that topic or have some tips, how I can await the response of the solr-client-search operation and then give it back in a sequential way?
Thanks for any help.
Udo Gerhards
over one week, it seems now that I have found a solution:
SolrManager.prototype.promisedQuery = function(query, callback) {
var solrClient = solr.createClient(this.configuration.cores.page);
var docs = null;
var deferred = Q.defer();
var request = solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
in all other managers, which are calling the above function:
...
var dbPromise = this.solrManager.promisedQuery(query);
var _self = this;
return Q.async(function*(){
var result = yield dbPromise;
return result;
});
...
After first tests, it seems that synchronized methods will wait until the promise is settled.
The only thing is, that it runs only with NodeJs version 0.11.10, which supports generator functions, with activated --harmony-flag and "q"-module.
Best regards
Udo
You are just using the promises a bit incorrectly. Instead of returning records, you need to return 'deferred.promise'. It should look something like this (note that you don't need the callback you passed into promisedQuery).
SolrManager.prototype.promisedQuery = function(query) {
var solrClient = solr.createClient(this.configuration.cores.page),
deferred = Q.defer();
solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
To use it you would do something like:
SolrManager.promisedQuery(myquery)
.then(function (data) {
// data is whatever your 'resolved' in promisedQuery
}, function (err) {
// err is whatever you rejected in promisedQuery
});
based on rquinns answer I've changed the code like follows:
SolrManager.prototype.promisedQuery = function(query, callback) {
var solrClient = solr.createClient(this.configuration.cores.page);
var docs = null;
var finished = false;
var deferred = Q.defer();
var request = solrClient.search(query, function(err,obj){
if (!err) {
if (obj.response.numFound > 0) {
deferred.resolve(obj.response.docs);
} else {
deferred.resolve(null);
}
} else {
deferred.reject(err);
}
});
return deferred.promise;
};
...
DemoObject.prototype.toString = function() {
return SolrManager.promisedQuery(this.query).then(function(result){
return result['title'];
}).fail(function(error){
return error;
});
};
DemoObject.prototype.typeOf = function() {
return SolrManager.promisedQuery(this.query).then(function(result){
return result['title'];
}).fail(function(error){
return error;
});
};
I think, this is the right way to use the "promise"-object. But what happens when i do the follwing:
...
var demoObject = new DemoObject();
demoObject.query = "id:1";
console.log(''+demoObject);
...
or if I use "demoObject" by concatenating it to a string
...
var string = "Some string "+demoObject;
...
In case of the string concatenation, I'm currently not sure that the string will contain also the title field from the database. Same for console output.
Will nodejs be so intelligent that it resolves for e.g. the string concatenation "after" the results from the database will be available?
BR
Udo