Reputation: 708
I'm refactoring some legacy code that I didn't originally write and I've come upon an issue with asynchronous data loading. The first time a particular modal is opened, a bunch of data representing a form object gets loaded. A function then cycles through the inputs of the form and fleshes them out as needed. It looks something like this (extremely simplified):
component.inputs.forEach(function(input) {
if (input.field == 'foo') {
input.cols = 5;
//etc.
}
if (input.field == 'bar') {
DataService.getBars().then(function(data){
data.forEach(function(e){
input.options.push(e.description);
});
};
}
if (input.field == 'baz') {
input.pattern = /regex/;
//etc.
}
});
return component;
The problem, of course, is that if my input.field is 'bar', the code continues running and hits the final return before the async call to DataService is resolved, so the first time the modal is opened, the input.options have not been filled out for 'bar' input.
Is it possible to make the code wait for the promise from the DataService to be resolved before continuing, or is there another way to handle the situation where in most cases the function is synchronous, but has to make an async call in only one case? Or have I shot myself in the foot by including an async call in this big chain of ifs?
Upvotes: 2
Views: 775
Reputation: 48968
One approach is to create a promise and attach it as a property to your returned object.
function getComponent() {
component.inputs.forEach(function(input) {
//create initial promise
var $promise = $q.when(input);
if (input.field == 'foo') {
input.cols = 5;
//etc.
}
if (input.field == 'bar') {
//chain from initial promise
$promise = $promise.then(function () {
//return promise for chaining
return getBarPromise(input);
});
}
//attach promise to input object
input.$promise = $promise;
});
var promises = [];
angular.forEach(inputs, function(input) {
promises.push(input.$promise);
});
//create composite promise
var $promise = $q.all(promises);
//final chain
$promise = $promise.then( function() {
//return component for chaining
return component;
});
//attach promise to component
component.$promise = $promise;
return component;
};
The returned component
object will eventually be filled in with the results of the service calls. Functions that need to wait for completion of all the service calls can chain from the attached $promise
property.
$scope.component = getComponent();
$scope.component.$promise.then( function (resolvedComponent) {
//open modal
}).catch( function(errorResponse) {
//log error response
});
Because calling the then
method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.1
Upvotes: 1
Reputation: 825
If you want to stay with your existing code structure and make this work, you probably will need to use promises
. You can also use javascript's map function
. Note: you would need to inject $q
into wherever you want to call this function.
function getComponent() {
var deferred = $q.defer(),
deferred2 = $q.defer(),
promises = component.inputs.map(function(input)) {
if (input.field == 'foo') {
input.cols = 5;
deferred2.resolve();
}
else if (input.field == 'bar') {
DataService.getBars().then(function(data) {
data.forEach(function(e){
input.options.push(e.description);
});
deferred2.resolve();
}).catch(function(err)) {
deferred2.reject(err);
});
}
else if (input.field == 'baz') {
input.pattern = /regex/;
deferred2.resolve();
}
return deferred2.promise;
});
$q.all(promises)
.then(function() {
deferred.resolve(component);
}).catch(function(err) {
deferred.reject(err);
});
return deferred.promise;
}
Once each input
in component.inputs
has been parsed appropriately, then the $q.all
block will trigger and you can return your new component
object.
Finally, to set your component
object, simply do the following:
getComponent().then(function(result)) {
//Set component object here with result
$scope.component = result;
}).catch(function(err) {
// Handle error here
});
Upvotes: 0