Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angularjs promise not binding to template in 1.2

Tags:

angularjs

After upgrading to 1.2, promises returned by my services behave differently... Simple service myDates:

getDates: function () {
           var deferred = $q.defer();

            $http.get(aGoodURL).
                 success(function (data, status, headers, config) {
                     deferred.resolve(data);  // we get to here fine.
            })......

In earlier version I could just do, in my controller:

$scope.theDates = myDates.getDates();

and the promises returned from getDates could be bound directly to a Select element. Now this doesn't work and I'm forced to supply a callback on the promise in my controller or the data wont bind:

$scope.theDates = matchDates.getDates();
$scope.theDates.then(function (data) {
      $scope.theDates = data;  // this wasn't necessary in the past

The docs still say:

$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.

They (promises) were working in older versions of Angular but in the 1.2 RC3 automatic binding fails in all my simple services.... any ideas on what I might be doing wrong.

like image 356
Pablo Avatar asked Oct 19 '13 22:10

Pablo


3 Answers

There are changes in 1.2.0-rc3, including one you mentioned:

AngularJS 1.2.0-rc3 ferocious-twitch fixes a number of high priority issues in $compile and $animate and paves the way for 1.2. This release also introduces some important breaking changes that in some cases could break your directives and templates. Please be sure to read the changelog to understand these changes and learn how to migrate your code if needed. For full details in this release, see the changelog.

There is description in change log:

$parse:

  • due to 5dc35b52, $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and if absolutely needed, it can be reenabled during transitional period via $parseProvider.unwrapPromises(true) api.
  • due to b6a37d11, feature added in rc.2 that unwraps return values from functions if the values are promises (if promise unwrapping is enabled - see previous point), was reverted due to breaking a popular usage pattern.
like image 72
Nenad Avatar answered Oct 16 '22 14:10

Nenad


As @Nenad notices, promises are no longer automatically dereferenced. This is one of the most bizarre decisions I've ever seen since it silently removes a function that I relied on (and that was one of the unique selling points of angular for me, less is more). So it took me quite a bit of time to figure this out. Especially since the $resource framework still seems to work fine. On top of this all, this is also a release candidate. If they really had to deprecate this (the arguments sound very feeble) they could at least have given a grace period where there were warnings before they silently shut it off. Though usually very impressed with angular, this is a big minus. I would not be surprised if this actually will be reverted, though there seems to be relatively little outcry so far.

Anyway. What are the solutions?

  • Always use then(), and assign the $scope in the then method

    function Ctrl($scope) {
       foo().then( function(d) { $scope.d = d; });
    )
    
  • call the value through an unwrap function. This function returns a field in the promise and sets this field through the then method. It will therefore be undefined as long as the promise is not resolved.

    $rootScope.unwrap = function (v) {
      if (v && v.then) {
        var p = v;
        if (!('$$v' in v)) {
          p.$$v = undefined;
          p.then(function(val) { p.$$v = val; });
        }
        v = v.$$v;
      }
      return v;
    };
    

    You can now call it:

    Hello {{ unwrap(world) }}.
    

    This is from http://plnkr.co/edit/Fn7z3g?p=preview which does not have a name associated with it.

  • Set $parseProvider.unwrapPromises(true) and live with the messages, which you could turn off with $parseProvider.logPromiseWarnings(false) but it is better to be aware that they might remove the functionality in a following release.

Sigh, 40 years Smalltalk had the become message that allowed you to switch object references. Promises as they could have been ...

UPDATE:

After changing my application I found a general pattern that worked quite well.

Assuming I need object 'x' and there is some way to get this object remotely. I will then first check a cache for 'x'. If there is an object, I return it. If no such object exists, I create an actual empty object. Unfortunately, this requires you to know if this is will be an Array or a hash/object. I put this object in the cache so future calls can use it. I then start the remote call and on the callback I copy the data obtained from the remote system in the created object. The cache ensures that repeated calls to the get method are not creating lots of remote calls for the same object.

 function getX() {
   var x = cache.get('x');
   if ( x == undefined) {
      cache.put('x', x={});
      remote.getX().then( function(d) { angular.copy(d,x); } );
   }
   return x;
 }

Yet another alternative is to provide the get method with the destination of the object:

 function getX(scope,name) {
   remote.getX().then( function(d) { 
       scope[name] = d;
   } );
 }       
like image 22
Peter Kriens Avatar answered Oct 16 '22 12:10

Peter Kriens


You could always create a Common angular service and put an unwrap method in there that sort of recreates how the old promises worked. Here is an example method:

var shared = angular.module("shared");

shared.service("Common", [
    function () {

        // [Unwrap] will return a value to the scope which is automatially updated. For example,
        //      you can pass the second argument an ng-resource call or promise, and when the result comes back
        //      it will update the first argument. You can also pass a function that returns an ng-resource or
        //      promise and it will extend the first argument to contain a new "load()" method which can make the
        //      call again. The first argument should either be an object (like {}) or an array (like []) based on
        //      the expected return value of the promise.
        // Usage: $scope.reminders = Common.unwrap([], Reminders.query().$promise);
        // Usage: $scope.reminders = Common.unwrap([], Reminders.query());
        // Usage: $scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
        // Usage: $scope.reminders.load();
        this.unwrap = function(result, func) {
            if (!result || !func) return result;

            var then = function(promise) {
                //see if they sent a resource
                if ('$promise' in promise) {
                    promise.$promise.then(update);
                }
                //see if they sent a promise directly
                else if ('then' in promise) {
                    promise.then(update);
                }
            };

            var update = function(data) {
                if ($.isArray(result)) {
                    //clear result list
                    result.length = 0;
                    //populate result list with data
                    $.each(data, function(i, item) {
                        result.push(item);
                    });
                } else {
                    //clear result object
                    for (var prop in result) {
                        if (prop !== 'load') delete result[prop];
                    }
                    //deep populate result object from data
                    $.extend(true, result, data);
                }
            };

            //see if they sent a function that returns a promise, or a promise itself
            if ($.isFunction(func)) {
                // create load event for reuse
                result.load = function() {
                    then(func());
                };
                result.load();
            } else {
                then(func);
            }

            return result;
        };
    }
]);

This basically works how the old promises did and auto-resolves. However, if the second argument is a function it has the added benefit of adding a ".load()" method which can reload the value into the scope.

angular.module('site').controller("homeController", function(Common) {
    $scope.reminders = Common.unwrap([], Reminders.query().$promise);
    $scope.reminders = Common.unwrap([], Reminders.query());
    $scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
    function refresh() {
        $scope.reminders.load();
    }
});
like image 3
pwhe23 Avatar answered Oct 16 '22 14:10

pwhe23