Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass a promise into an Angular-UI state controller

Is it possible to pass a promise to a UI.Router $state from an outside controller (e.g. the controller that triggered the state)?

I know that $state.go() returns a promise; is it possible to override that with your own promise resolve this promise directly yourself or resolve it using a new promise?

Also, the documentation says the promise returned by $state.go() can be rejected with another promise (indicated by transition superseded), but I can't find anywhere that indicates how this can be done from within the state itself.

For example, in the code below, I would like to be able to wait for the user to click on a button ($scope.buttonClicked()) before continuing on to doSomethingElse().

I know that I can emit an event, but since promises are baked into Angular so deeply, I wondered if there was a way to do this through promise.resolve/promise.reject.

angular.module('APP', ['ui.router'])
.config(['$stateProvider', function ($stateProvider) {
    $stateProvider
    .state('myState', {
        template: '<p>myState</p>',
        controller: ['$state', '$scope', '$q', function ($state, $scope, $q) {
            var deferred = $q.defer();

            $scope.buttonClicked = function () {
                deferred.resolve();
            }
        }]
    });
}])
.controller('mainCtrl', ['$state', function ($state) {
    $state.go('myState')
    .then(doSomethingElse)
}]);

Update I have accepted @blint's answer as it has got me closest to what I wanted. Below is some code that fleshes out this answer's idea a bit more. I don't think the way I have written this is a very elegant solution and I am happy if someone can suggest a better way to resolve promises from a triggered state.

The solution I've chosen is to chain your promises as you normally would in your controller, but leave a $scope.next() method (or something similar) attached to that scope that resolves/rejects the promise. Since the state can inherit the calling controller's scope, it will be able to invoke that method directly and thus resolve/reject the promise. Here is how it might work:

First, set up your states with buttons/controllers that call a $scope.next() method:

.config(function ($stateProvider) {
    $stateProvider
    .state('selectLanguage', {
        template: '<p>Select language for app: \
            <select ng-model="user.language" ng-options="language.label for language in languages">\
                <option value="">Please select</option>\
            </select>\
            <button ng-click="next()">Next</button>\
            </p>',
        controller: function ($scope) {
            $scope.languages = [
                {label: 'Deutch', value: 'de'},
                {label: 'English', value: 'en'},
                {label: 'Français', value: 'fr'},
                {label: 'Error', value: null}
            ];
        }
    })
    .state('getUserInfo', {
        template: '<p>Name: <input ng-model="user.name" /><br />\
            Email: <input ng-model="user.email" /><br />\
            <button ng-click="next()">Next</button>\
            </p>'
    })
    .state('mainMenu', {
        template: '<p>The main menu for {{user.name}} is in {{user.language.label}}</p>'
    })
    .state('error', {
        template: '<p>There was an error</p>'
    });
})

Next, you set up your controller. In this case, I'm using a local service method, user.loadFromLocalStorage() to get the ball rolling (it returns a promise), but any promise will do. In this workflow, if the $scope.user is missing anything, it will progressively get populated using states. If it is fully populated, it skips right to the main menu. If elements are left empty or are in an invalid state, you get taken to an error view.

.controller('mainCtrl', function ($scope, $state, $q, User) {
    $scope.user = new User();

    $scope.user.loadFromLocalStorage()
    .then(function () {
        var deferred;

        if ($scope.user.language === null) {
             deferred = $q.defer();

             $state.go('selectLanguage');

             $scope.next = function () {
                $scope.next = undefined;

                if ($scope.user.language === null) {
                    return deferred.reject('Language not selected somehow');
                }

                deferred.resolve();
             };

             return deferred.promise;
        }
    })
    .then(function () {
        var deferred;

        if ($scope.user.name === null || $scope.user.email === null) {
            deferred = $q.defer();

            $state.go('getUserInfo');
            $scope.next = function () {
                $scope.next = undefined;

                if ($scope.user.name === null || $scope.user.email === null) {
                    return deferred.reject('Could not get user name or email');
                }

                deferred.resolve();
            };

            return deferred.promise;
        }


    })
    .then(function () {
        $state.go('mainMenu');
    })
    .catch(function (err) {
        $state.go('error', err);
    });

});

This is pretty verbose and not yet very DRY, but it shows the overall intention of asynchronous flow control using promises.

like image 326
Andrew Avatar asked Sep 30 '22 20:09

Andrew


1 Answers

The purpose of promises is to guarantee a result... or handle a failure. Promises can be chained, returned in functions and thus extended.

You would have no interest in "overriding" a promise. What you can do, however:

  • Handle the failure case. Here's the example from the docs:
promiseB = promiseA.then(function(result) {
    // success: do something and resolve promiseB
    //          with the old or a new result
    return result;
  }, function(reason) {
    // error: handle the error if possible and
    //        resolve promiseB with newPromiseOrValue,
    //        otherwise forward the rejection to promiseB
    if (canHandle(reason)) {
     // handle the error and recover
     return newPromiseOrValue;
    }
    return $q.reject(reason);
  });
  • Append a new asynchronous operation in the promise chain. You can combine promises. If a method called in the chain returns a promise, the parent promised will wall the rest of the chain once the new promise is resolved.

Here's the pattern you might be looking for:

angular.module('APP', ['ui.router'])
.config(['$stateProvider', function ($stateProvider) {
    $stateProvider
    .state('myState', {
        template: '<p>myState</p>',
        controller: 'myCtrl'
    });
}])
.controller('myCtrl', ['$scope', '$state', '$q', '$http', 'someAsyncServiceWithCallback',
    function ($scope, $state, $q, $http, myService) {
    $scope.buttonClicked = function () {
        $state.go('myState')
        .then(function () {
            // You can return a promise...
            // From a method that returns a promise
            // return $http.get('/myURL');

            // Or from an old-school method taking a callback:
            var deferred = $q.defer();
            myService(function(data) {
                deferred.resolve(data);
            });

            return deferred.promise;
        },
        function () {
            console.log("$state.go() failed :(");
        });
    };
}]);
like image 101
ngasull Avatar answered Oct 06 '22 01:10

ngasull