2

I have a service that does something hard and returns a promise:

.factory('myService', function($q) {

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

            setTimeout(function() {
                deferred.resolve("I'm done!");
            }, 1000);

            return deferred.promise;
        }
    };
})

I have a controller that adds a function to the scope using that service:

.controller('MyCtrl', function($scope, myService) {

    $scope.doSomething = function() {
        var promise = myService.doSomethingHard();

        promise.then(function(result) {
            alert(result);
        });
    };

})

I use a directive to call that controller function by parsing an attribute:

.directive('myDirective', function($parse) {
    return {
        link: function(scope, el, attr) {

            var myParsedFunction = $parse(attr.myDirective);

            el.bind('click', function() {
                myParsedFunction(scope);
            });
        }
    };
})

with the template

<div ng-controller="MyCtrl">
    <button my-directive="doSomething()">The Button</button>
</div>

Clicking the button triggers the event listener, which calls the controller function doSomething, which calls the service function doSomethingHard, which returns a promise, THAT IS NEVER RESOLVED.

Whole thing up on a fiddle here:

http://jsfiddle.net/nicholasstephan/RgKaT/

What gives?

Thanks.

EDIT: Thanks to Maksym H., It looks like wrapping the promise resolve in $scope.$apply() makes it fire in the controller. I've got a working fiddle up http://jsfiddle.net/RgKaT/4/. But I'd really like to keep the scope out of my services.

I'd also really like to know why this works. Or better yet, why it doesn't work without resolving the promise while wrapped in a scope apply. The whole Angular world vs regular Javascript world analogy makes sense when thinking about properties as changes need to be digested, but this is a promise... with callback functions. Does $q just flag the promise as resolved and wait for the scope to digest that property update and fire its resolved handler functions?

nicholas
  • 14,184
  • 22
  • 82
  • 138

2 Answers2

2

Here is another way: Try to define scope in directive and bind this attr to expect parent scope.

.directive('myDirective', function() {
  return {
    scope: { myDirective: "=" }, // or { myParsedFunction: "=myDirective" },
    link: function(scope, el, attr) {

        el.bind('click', function() {
            scope.myDirecive(scope); // or scope.myParsedFunction(scope)
        });
    }
  };
})

But the main thing is to run digest when you resolving it after some time:

.factory('myService', function($q, $timeout) {

    return {
        doSomethingHard: function() {
            alert('3. doing something hard');

            var deferred = $q.defer();

            // using $timeout as it's working better with promises
            $timeout(function() {
                alert('4. resolving deferred');
                deferred.resolve('Hello World!'); // Here...
            }, 1000);

            return deferred.promise;
        }
    };
})

jsFiddle

P.S. Make sure you are passing method as model of parent scope not applying this by "()" in HTML

<button my-directive="doSomething">Button</button>

Mak
  • 19,913
  • 5
  • 26
  • 32
  • @nicholas, I've updated the code on Plunker. I hope it will work for you. – Mak Apr 21 '13 at 22:28
  • Awesome answer. Thanks. I've got it to work by adding $rootScope to the service like you suggest. Thanks. But I'm still not really happy, nor do I understand, with that solution (see my edit). – nicholas Apr 22 '13 at 20:18
  • @nicholas, you don't need to worry, because it's fault of native setTimeout. With using this, agnular seems to stop watching for resolving. In real example you will might use something like XHR erquest and resolve on success, right? But now you can test it out using angular's $timeout provider that works better with angular promises. And as well you can try to resolve with XHR request and it should work. Here is updated fiddle (http://jsfiddle.net/RgKaT/6/) – Mak Apr 23 '13 at 08:44
  • Ah. That makes some sense. Parsing an attribute so I don't have to create an isolated scope for the directive, http://jsfiddle.net/jYZR9/1/, works too. In my production code, which is identical except for using an $http instead of a $timeout, I'm still having to write `$scope.$apply(fn)` instead of just `fn()`, which is a bit odd, but the issue must stem from somewhere else. I'll try to duplicate in a fiddle for my own education but for now it's working, which is good enough for me. Thanks. – nicholas Apr 26 '13 at 15:25
  • Won't this return complete after 1 second even if the promise has not resolved? – Ray Suelzer Oct 22 '13 at 23:38
  • @RaySülzer You're right, but I guess this is just the rough example like "what is the sequence of promises". Btw For better understanding I suggest everybody to visit this amazing detailed promises explanation http://stackoverflow.com/questions/15604196/promises-in-angularjs-and-where-to-use-them – Mak Oct 24 '13 at 21:50
1

Just replace setTimeout with $timeout (and remember to inject $timeout into your service). Here's an updated jsFiddle.

Fred Sauer
  • 1,012
  • 9
  • 20