3

Disclaimer: there actually two questions being asked here but I feel like they are closely related.

I'm trying to pass a promise object to a directive and I want to run some initialization code in the directive as soon as the promise resolves.

In a controller I have:

$scope.item = $http.get(...)
    .success(function (result) {
        $scope.item = result.item;
});

$scope.item is passed to a directive (via an attribute in an isolated scope called item). The directive link function finally do something like:

Promise.resolve(scope.item)
    .then(function () {
        initialize();
});

This works fine in Firefox but when I run it on IE I get an error because Promise is not defined. This problem made me realize I need to probably use the AngularJS $q service in order to provide consistency among browsers and while I was looking at the documentation I discovered another problem, which seemed small at first but it is actually giving me headaches: the success() function is deprecated and I should use then(successCallback) instead. Easy peasy, I thought, BUT as soon as I change the success call in the controller the code stop working in Firefox too! I cannot figure out why. So this is the first question.

The second question is that (even if I leave the success call in the controller) if I modify the code in the directive link function to use $q with what I thought was the equivalent:

$q.resolve(scope.item, function() { initialize(); });

this still doesn't work at all. Any suggestion?

  • 1
    Be aware that you're overwriting `$scope.item` in your callback function. In the first time, `$scope.item` is a promise, but after the promise is resolved, you're assigning `result.item` to it and then `$scope.item` becomes your retrieved data... – philsch Dec 29 '15 at 17:43
  • @philsch this could be the culprit but I'm failing to understand how to pass workaround the double assignment without passing and undefined property to the directive scope –  Dec 29 '15 at 18:03
  • Please have a look at the solution I've posted, hope this helps with your problem. – philsch Dec 30 '15 at 07:58

4 Answers4

4

You need to use Angular's $q not only because it works across browsers - but also because it is deeply linked to Angular's digest cycles. Other promise libraries can accomplish this feat but native promises cannot easily do so.

What $q.when does (the $q version of Promise.resolve) is convert a value or a promise to a $q promise. You don't need to do it since you're already using Angular's own $http API which returns promises already.

I warmly recommend that you put your web calls in services and don't affect the scope directly - and then call those services to update the scope.

The pattern is basically:

$http.get(...) // no assign
    .success(function (result) { // old API, deprecated
      $scope.item = result.item; // this is fine
});

Or with the better then promise API that has the benefits of promises over callbacks like chaining and error handling:

$http.get(...).then(function (result) {
  $scope.item = result.data;
});
Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • I want it to work even if I do not use $http (testing, for example). The http call it's actually wrapped in a service I could mock. –  Dec 29 '15 at 17:43
  • @Lennox "wrapping" the `then` API with a mock is trivial `$q.when("your mock data here")` would work just fine. If you put it in a service (like I suggested) mocking it becomes trivial. – Benjamin Gruenbaum Dec 29 '15 at 17:45
  • I will give you an upvote if you change that `.success` method example to `.then`. `.then` is **deprecated** https://docs.angularjs.org/api/ng/service/$http#deprecation-notice – georgeawg Dec 29 '15 at 17:49
  • @georgeawg that's fine, you're welcome to not upvote my answer if you don't think it's good. I've added a notice that it's deprecated and there already was a `then` example with "you should totally use this and not the other one" instead. – Benjamin Gruenbaum Dec 29 '15 at 17:51
  • what's not really clear to me is that if in the controller I don't assign $scope.item to the result of the $http.get then when I pass $scope.item to the directive it is undefined and it breaks there... –  Dec 29 '15 at 18:02
  • 1
    @Lennox You pass the promise around and then have it populate the data wherever you need it. – Benjamin Gruenbaum Dec 29 '15 at 18:55
  • @BenjaminGruenbaum yep but in your code I have no promise in the controller until the $http.get returns, which is too late. I have to assign the result of the $http.get (promise) to the object I intend to share with the directive, please see my solution. –  Dec 30 '15 at 08:20
  • @Lennox no no, pass the _promise_ around, not the data. Don't pass `$scope.item` pass the promise for it. Although the controller should not make HTTP calls for its directives anyway - that should be done in an injected service. – Benjamin Gruenbaum Dec 30 '15 at 08:36
  • @BenjaminGruenbaum I'm not sure I understand what you mean, doing this `$scope.item = $http.get(...).then(function (result) { return result.data.item; });` is equivalent to pass the promise around, because I then pass `$scope.item` to the directive, isn't it? –  Dec 30 '15 at 11:17
  • Something like https://jsfiddle.net/ujjo13cs/ , this is no longer the original question though. – Benjamin Gruenbaum Dec 30 '15 at 11:29
  • @BenjaminGruenbaum it's still the original question and we are saying the same thing. Also, look at my version of your example, this is how I solved it in my code: [jsfiddle](https://jsfiddle.net/ujjo13cs/1/). –  Dec 30 '15 at 11:57
0

You are correct about the .success method being deprecated. The .then method returns data differently than the .success method.

$scope.httpPromise = $http.get(...)
    .then(function (result) {
        $scope.item = result.data.item;
        return result.data;
});

You need to return the data to chain from it.

$scope.httpPromise.then ( function (data) {
     //Do something with data
     initialize();
});

For more information on the deprecation of the .success method see AngularJS $http Service API Reference -- deprecation notice.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
-1

Since scope.item is a promise all you have to do is:

scope.item.resolve.then(function() { initialize(); });

Make sure $q is injected in your directive.

jbrown
  • 3,025
  • 1
  • 15
  • 22
  • it doesn't seem to work but anyway I can't assume scope.item is a promise, that's why I used Promise.resolve() in the first place. –  Dec 29 '15 at 17:39
  • That's because $scope.item isn't returning anything. Should be: $scope.item = $http.get(...) .success(function (result) { return result.item; }); – jbrown Dec 29 '15 at 17:41
  • Gotta love the down vote based on lack of understanding rather than the answer being incorrect. – jbrown Dec 29 '15 at 17:51
  • I didn't downvote, but this isn't really a complete answer to the question. – Benjamin Gruenbaum Dec 29 '15 at 18:01
-1

improved answer

As @benjamin-gruenbaum mentioned correctly, I used an anti-pattern in my answer. So the solution is basically to pass the promise to your directive and use it there (as already mentioned in Benjamins answer).

Working jsFiddle: https://jsfiddle.net/eovp82qw/1/


old answer, uses anti-pattern

Sorry if I give you a solution that perhaps differs too much from your code. But maybe you can adopt it to your solution.

My approach would be to create a second promise I handover to your directive. This is imho a cleaner way for resolving the waiting state of the directive and don't reuse the same scope variable for two different tasks.

HTML

<body ng-app="myApp">
    <div ng-controller="MyCtrl">
        <my-directive promise="promiseFromController"></my-directive>
    </div>
</body>

JS

function MyCtrl($scope, $q, $http) {
    function init() {
        var deferredCall = $q.defer();

        // simulated ajax call to your server
        // the callback will be executed async
        $http.get('/echo/json/').then(function(data) {
            console.log('received', data);
            deferredCall.resolve(data); //<-- this will resolve the promise you'll handover to your directive
        });

        // we're return our promise immediately
        $scope.promiseFromController = deferredCall.promise;
    }
    init();
 }

angular.module('myApp',[])
.controller('MyCtrl', MyCtrl)
.directive('myDirective', function() {
    return {
        scope: {
            promise: '='
        },
        controller: function($scope) {
            console.log($scope);
            $scope.promise.then(function(data) {
                console.log('received data in directive', data);
            });
         }
    }
})

Working jsFiddle: https://jsfiddle.net/1ua4r6m0/ (There is no output, check your browser console ;) )

philsch
  • 1,004
  • 11
  • 19