Question

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.

Was it helpful?

Solution

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 :(");
        });
    };
}]);

OTHER TIPS

Perhaps one way of achieving this would be to return your promise from the state's resolve

resolve: {
    myResolve: function($scope, $q) {
        var deferred = $q.defer();
        $scope.buttonClicked = function () {
           deferred.resolve();
        }
        return deferred.promise;
    }
}

There is also an example in resolve docs that may be of interest

 // Another promise example. If you need to do some 
 // processing of the result, use .then, and your 
 // promise is chained in for free. This is another
 // typical use case of resolve.
 promiseObj2:  function($http){
    return $http({method: 'GET', url: '/someUrl'})
       .then (function (data) {
           return doSomeStuffFirst(data);
       });
 },
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top