68

I am trying to bind a promise to a view. I don't know if you can do that directly, but that's what I'm attempting to do. Any ideas what I am doing wrong?

Note: the source is a little contrived with the timeout and uses static data, but that's to make the code easier to diagnose.

EDIT: JSFiddle Page: http://jsfiddle.net/YQwaf/27/

EDIT: SOLUTION: It turned out you can directly bind promises. I had two problems with my original code:

  1. Using setTimeout() instead of angular's $timeout was a problem. Angular doesn't know it needs to refresh the UI when the timeout is triggered ( You could solve this with $scope.$apply inside setTimeout, or you can just use $timeout )
  2. Binding to a function that returned a promise was a problem. If it gets called a second time, it makes yet another promise. Better is to set a scope variable to the promise and only create a new promise as needed. (In my case, this was calling $scope.$watch on the Country Code)

HTML:

<div ng:controller="addressValidationController">
    Region Code <select ng:model="regionCode" ng:options="r.code as r.name for r in getRegions()"/>
    Country Code<select ng:model="countryCode"><option value="US">United States</option><option value="CA">Canada</option></select>
</div>

JS:

function addressValidationController($scope, $q) {
    var regions = {
        US: [{code: 'WI',name: 'Wisconsin'}, {code: 'MN',name: 'Minnesota'}], 
        CA: [{code: 'ON',name: 'Ontario'}]
    };
    $scope.getRegions = function () {
        var deferred = $q.defer();
        setTimeout(function () {
            var countryRegions = regions[$scope.countryCode];
            console.log(countryRegions);
            if(countryRegions === undefined) {
                deferred.resolve([]);
            } else {
                deferred.resolve(countryRegions);
            }
        }, 1000);
        return deferred.promise;
    };
}
4
  • Could you make a working fiddle? That would be great! Commented Feb 21, 2013 at 3:25
  • Actually it's in the doc: docs.angularjs.org/api/ng.$q Commented Feb 21, 2013 at 3:49
  • 2
    There's a little caveat you should be aware of: not only the UI is updated only if $scope.$apply is called, but every callback registered on the promise will not br called if the deferred is resolved outside $apply :) Commented Mar 14, 2013 at 18:30
  • Note that the accepted answer and/or the solution in the question no longer work - see my answer below. Commented Jul 7, 2015 at 21:49

4 Answers 4

71

As of Angular 1.2, you can't use promises in templates directly anymore.
Instead, you need to put the result into $scope inside then, like you normally would—no magic.

As a temporary workaround to get the old behavior, you can call

$parseProvider.unwrapPromises(true)

but this feature will be removed later on, so don't depend on it.

Sign up to request clarification or add additional context in comments.

1 Comment

Timely and helpful answer! I was just barely dead-ending in a tutorial from an older version of Angular.
29

WARNING: this answer was accurate when it was written, but as of 1.2 the Angular template engine does not handle promises transparently! -- @Malvolio

Yes the template engine (and expressions) handle promises transparently, but I would assign the promise to a scope property in the controller and not call everytime a function that returns a new promise (I think it's your problem, resolved promise is lost because a new promise is returned everytime).

JSFiddle: http://jsfiddle.net/YQwaf/36/

HTML:

<div ng:controller="addressValidationController">
    Region Code <select ng:model="regionCode" ng:options="r.code as r.name for r in regions"/>
    Country Code<select ng:model="countryCode"><option value="US">United States</option><option value="CA">Canada</option></select>
</div>

JS:

function addressValidationController($scope, $q, $timeout) {
    var regions = {
        US: [{
            code: 'WI',
            name: 'Wisconsin'},
        {
            code: 'MN',
            name: 'Minnesota'}],
        CA: [{
            code: 'ON',
            name: 'Ontario'}]
    };

    function getRegions(countryCode) {
        console.log('getRegions: ' + countryCode);
        var deferred = $q.defer();
        $timeout(function() {
            var countryRegions = regions[countryCode];
            if (countryRegions === undefined) {
                console.log('resolve empty');
                deferred.resolve([]);
            } else {
                console.log('resolve');
                deferred.resolve(countryRegions);
            }
        }, 1000);
        return deferred.promise;
    };

    $scope.regions = [];

    // Manage country changes:
    $scope.$watch('countryCode', function(countryCode) {
        if (angular.isDefined(countryCode)) {
            $scope.regions = getRegions(countryCode);
        }
        else {
            $scope.regions = [];
        }
    });
}​

6 Comments

The problem is the regions are dependent on the countryCode field. (when you change the country, your list of state changes)
I got it working: jsfiddle.net/YQwaf/31 Update your answer and I'll give you credit :)
Thanks, I made some little edits in the fiddle (you can directly use the promise like suggested initially)
Hey Guillaume86, I think you might be able to help me with this one. My promise resolves to an array, and updating a single item in that array won't update my scope. stackoverflow.com/questions/14313573/…
WARNING: this answer was accurate when it was written, but as of 1.2 the Angular template engine does not handle promises transparently!
|
27

As of Angular 1.3 - $parseProvider.unwrapPromises(true) will no longer work.

Instead, you should unwrap the promises directly:

myApiMethod().then(function(value){
    $scope.item = value; 
});

Note that promise unwrapping will still work with ngResource as usual.

4 Comments

Is there a way to do this without needing to call $scope.$apply() after setting scope value from a promise? (Using Angular 1.3)
You don't need to call $scope.$apply() it is done automatically for you by the then handler (schedules a digest if there is not one in progress).
Well, not exactly, but the digest will automatically be applied IF and only if you are using the $http method (which has a special handler to do that). I was originally using jQuery's $.ajax method, and that's why it wasn't working when I was changing the scope after the promise returned. Changing it to use Angular's $http solved that issue. Thanks Benjamin.
@RyanWeiss it's not specific to $http it's specific to promises created with Angular promises ($q). If you'd like your $.ajax to do that you can cast it to one by $q.when(yourPromise). It's better to use $http for http requests in angular anyway.
0

returning a reference to the scope variable holding the list should suffice.

function addressValidationController($scope,$timeout) {
    var regions = {
        US: [{code: 'WI',name: 'Wisconsin'}, {code: 'MN',name: 'Minnesota'}], 
        CA: [{code: 'ON',name: 'Ontario'}]
    };

    $scope._regions = [];

    $scope.getRegions = function () {

        $timeout(function () {
            var countryRegions = regions[$scope.countryCode];
            console.log(countryRegions);
            if(countryRegions === undefined) {
                $scope._regions = []
            } else {
                $scope._regions = countryRegions
            }
        }, 1000);

        return $scope._regions;
    };
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.