Nat Webb
Nat Webb

Reputation: 708

Possible Async Call in a Conditional

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

Answers (2)

georgeawg
georgeawg

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

vlin
vlin

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

Related Questions