Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS promises notify not working

I have the following controller code:

 .controller('Controller1', function ($scope, MyService) {

    var promise = MyService.getData();
    promise.then(function(success) {
        console.log("success");
    }, function(error) {
        console.log("error");
    }, function(update) {
        console.log("got an update!");
    }) ;

}

And in my services.js:

 .factory('MyService', function ($resource, API_END_POINT, localStorageService, $q) {
   return {
       getData: function() {
           var resource = $resource(API_END_POINT + '/data', {
               query: { method: 'GET', isArray: true }
           });

           var deferred = $q.defer();
           var response = localStorageService.get("data");
           console.log("from local storage: "+JSON.stringify(response));
           deferred.notify(response);

           resource.query(function (success) {
               console.log("success querying RESTful resource")
               localStorageService.add("data", success);
               deferred.resolve(success);
           }, function(error) {
               console.log("error occurred");
               deferred.reject(response);
           });

           return deferred.promise;
       }
   }

})

But for some reason the deferred.notify call never seems to execute and be received within the controller. Have I don't something wrong here? I'm not sure how to get the notify to execute.

like image 519
jcm Avatar asked Jan 28 '14 09:01

jcm


3 Answers

I managed to get it working by wrapping notify in $timeout function:

$timeout(function() {
  deferred.notify('In progress')
}, 0)

Looks like you cant call notify before you return promise object, that kinda makes sense.

like image 188
kubiq Avatar answered Nov 16 '22 12:11

kubiq


source: http://www.bennadel.com/blog/2800-forcing-q-notify-to-execute-with-a-no-op-in-angularjs.htm

Forcing $q .notify() To Execute

The beauty of the .notify() event is that our data-access layer can use it serve up the "immediately available, yet stale" data while still using the .resolve() event for nothing but live data. This gives the calling context - your controller - great insight and control over which dataset is cached and whether or not it [the controller] even wants to incorporate cached data.

But, we run into a little bit of a race condition. The data-access service, that owns the cached data, needs to call .notify() before it returns the promise to the calling context. This means that your controller binds to the notify event after .notify() has been called. From a philosophical standpoint, this should be fine - Promises (and just about everything that is event-driven) are intended to invoke bindings asynchronously in order to create uniformity of access.

From a practical standpoint, however, it's not quite that simple. While AngularJS follows this philosophy, it also adds a few optimizations to cut down on processing. In our case specifically, AngularJS won't schedule the callback-processing in a deferred object unless it sees that at least one callback is bound (otherwise it thinks the world isn't listening). As such, our controller will never be notified about the cached data.

To get around this, we can have our service layer bind a no-op (no operation) function to the notify event before it calls .notify(). This way, when it does call .notify(), AngularJS will see that at least one callback is registered and it will scheduled a flushing of the pending queue in the next tick (which is implemented via $rootScope.$evalAsync()). This allows our controller to get notified of cached data even if it binds to the notify event after .notify() has been invoked.

To see this in action, I've created a friendService that returns data through two different methods. Both of the methods attempt to return cached data via .notify() and then "live" data via .resolve(). The only difference between the two methods is that one binds a no-op to the notify event before calling .notify()

<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />

<title>
    Forcing $q .notify() To Execute With A No-Op In AngularJS
</title>

<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

<h1>
    Forcing $q .notify() To Execute With A No-Op In AngularJS
</h1>

<h2>
    Friends
</h2>

<div ng-switch="isLoading">

    <!-- Show while friends are being loaded. -->
    <p ng-switch-when="true">
        <em>Loading...</em>
    </p>

    <!-- Show once the friends have loaded and are available in the view-model. -->
    <ul ng-switch-when="false">
        <li ng-repeat="friend in friends track by friend.id">
            {{ friend.name }}
        </li>
    </ul>

</div>

<p>
    <a ng-click="load()">Load</a>
    &nbsp;|&nbsp;
    <a ng-click="loadWithNoop()">Load With No-Op</a>
</p>


<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.13.min.js"></script>
<script type="text/javascript">

    // Create an application module for our demo.
    var app = angular.module( "Demo", [] );


    // -------------------------------------------------- //
    // -------------------------------------------------- //


    // I control the root of the application.
    app.controller(
        "AppController",
        function( $scope, friendService ) {

            $scope.isLoading = false;

            $scope.friends = [];

            // Load the friend data (defaults to "get" method vs. "getWithNoop").
            loadRemoteData();


            // ---
            // PUBLIC METHODS.
            // ---


            // I reload the list of friends using friendService.get().
            $scope.load = function() {

                loadRemoteData( "get" );

            };


            // I reload the list of friends using friendService.getWithNoop().
            $scope.loadWithNoop = function() {

                loadRemoteData( "getWithNoop" );

            };


            // ---
            // PRIVATE METHODS.
            // ---


            // I load the friends from the friend repository. I am passing-in the
            // method name to demonstrate that, from the Controller's point-of-view,
            // nothing here is different other than the name of the method. The real
            // substantive difference exists in the implementation of the friend-
            // Service method and how it interacts with $q / Deferred.
            function loadRemoteData( loadingMethod ) {

                console.info( "Loading friends with [", loadingMethod, "]" );

                // Indicate that we are in the loading phase.
                $scope.isLoading = true;

                // When we make the request, we expect the service to try to use
                // cached-data, which it will make available via the "notify" event
                // handler on the promise. As such, we're going to wire up the same
                // event handler to both the "resolve" and the "notify" callbacks.
                friendService[ loadingMethod || "get" ]
                    .call( friendService )
                    .then(
                        handleResolve, // Resolve.
                        null,
                        handleResolve // Notify.
                    )
                ;

                function handleResolve( friends ) {

                    // Indicate that the data is no longer being loaded.
                    $scope.isLoading = false;

                    $scope.friends = friends;

                    console.log( "Friends loaded successfully at", ( new Date() ).getTime() );

                }

            }

        }
    );


    // -------------------------------------------------- //
    // -------------------------------------------------- //


    // I provide access to the friend repository.
    app.factory(
        "friendService",
        function( $q, $timeout ) {

            // Our friend "repository".
            var friends = [
                {
                    id: 1,
                    name: "Tricia"
                },
                {
                    id: 2,
                    name: "Heather"
                },
                {
                    id: 3,
                    name: "Kim"
                }
            ];

            // Return the public API.
            return({
                get: get,
                getWithNoop: getWithNoop
            });


            // ---
            // PUBLIC METHODS.
            // ---


            // I return the list of friends. If the friends are cached locally, the
            // cached collection will be exposed via the promise' .notify() event.
            function get() {

                var deferred = $q.defer();

                // Notify the calling context with the cached data.
                deferred.notify( angular.copy( friends ) );

                $timeout(
                    function networkLatency() {

                        deferred.resolve( angular.copy( friends ) );

                    },
                    1000,
                    false // No need to trigger digest - $q will do that already.
                );

                return( deferred.promise );

            }


            // I return the list of friends. If the friends are cached locally, the
            // cached collection will be exposed via the promise' .notify() event.
            function getWithNoop() {

                var deferred = $q.defer();

                // -- BEGIN: Hack. ----------------------------------------------- //
                // CAUTION: This is a work-around for an optimization in the way
                // AngularJS implemented $q. When we go to invoke .notify(),
                // AngularJS will ignore the event if there are no pending callbacks
                // for the event. Since our calling context can't bind to .notify()
                // until after we invoke .notify() here (and return the promise),
                // AngularJS will ignore it. However, if we bind a No-Op (no
                // operation) function to the .notify() event, AngularJS will
                // schedule a flushing of the deferred queue in the "next tick,"
                // which will give the calling context time to bind to .notify().
                deferred.promise.then( null, null, angular.noop );
                // -- END: Hack. ------------------------------------------------- //

                // Notify the calling context with the cached data.
                deferred.notify( angular.copy( friends ) );

                $timeout(
                    function networkLatency() {

                        deferred.resolve( angular.copy( friends ) );

                    },
                    1000,
                    false // No need to trigger digest - $q will do that already.
                );

                return( deferred.promise );

            }

        }
    );

</script>

</body>
</html>

As you can see, the controller binds the same handler to the "resolve" and "notify" event of the promise. In this way, it can handle the cached data and the live data uniformly. The only difference is in which service-layer method it invokes - get() vs. getWithNoop(). And, if we invoke .get() a few times and then .getWithNoop() a few times, we can see the difference in the console.

like image 43
dryymoon Avatar answered Nov 16 '22 14:11

dryymoon


I tried to reproduce your problem here. It seems, that you cannot call notifyon the promise directly, but have to wrap into an $applycall.

See also the documentation for $q here.

To quote the exact lines from the example:

since this fn executes async in a future turn of the event loop, we need to wrap our code into an $apply call so that the model changes are properly observed.

You could try this your self and change your code a little bit:

deferred.notify(response); // should not work

resource.query(function (success) {
    deferred.notify('Returning from resource'); // should work
    console.log("success querying RESTful resource")
    localStorageService.add("data", success);
    deferred.resolve(success);
}, function(error) {
    deferred.notify('caught error!'); //should also work
    console.log("error occurred");
    deferred.reject(response);
});
like image 1
Florian Avatar answered Nov 16 '22 14:11

Florian