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;
    };
}
Adam Tegen
  • 25,378
  • 33
  • 125
  • 153

4 Answers4

70

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.

Alireza
  • 100,211
  • 27
  • 269
  • 172
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
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 = [];
        }
    });
}​
Robin van Baalen
  • 3,632
  • 2
  • 21
  • 35
Guillaume86
  • 14,341
  • 4
  • 53
  • 53
  • The problem is the regions are dependent on the countryCode field. (when you change the country, your list of state changes) – Adam Tegen Oct 23 '12 at 15:27
  • I got it working: http://jsfiddle.net/YQwaf/31/ Update your answer and I'll give you credit :) – Adam Tegen Oct 23 '12 at 16:25
  • Thanks, I made some little edits in the fiddle (you can directly use the promise like suggested initially) – Guillaume86 Oct 23 '12 at 17:01
  • 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. http://stackoverflow.com/questions/14313573/angularjs-how-to-force-a-scope-to-update-promise-for-array-item – winduptoy Jan 14 '13 at 19:37
  • 24
    **WARNING**: this answer was accurate when it was written, but as of 1.2 [the Angular template engine does not handle promises transparently](https://github.com/angular/angular.js/commit/5dc35b527b3c99f6544b8cb52e93c6510d3ac577)! – Michael Lorton Dec 20 '13 at 01:55
  • @Malvolio, hey yellow stockings cross-gartered, +1. – Roamer-1888 Sep 17 '14 at 12:17
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.

ndequeker
  • 7,932
  • 7
  • 61
  • 93
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Is there a way to do this without needing to call $scope.$apply() after setting scope value from a promise? (Using Angular 1.3) – Ryan Weiss Dec 12 '14 at 17:23
  • 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). – Benjamin Gruenbaum Dec 12 '14 at 17:24
  • 3
    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. – Ryan Weiss Dec 13 '14 at 19:30
  • 8
    @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. – Benjamin Gruenbaum Dec 13 '14 at 19:33
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;
    };
}