Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular - Bind to function that returns a promise

I am new to angular, and I am having a tough time getting to the bottom of this problem.

I am writing a single-page application, and am working on the authentication portion. I have a service called "sessionService" that I want to be able to use throughout the app to determine if the user is logged in or not. It is simple if I do something like this:

...service('sessionService', function(...) { 
    /*...snip...*/
    this.isLoggedIn = function() { 
        return this.authenticated;
     };
});

Where "authenticated" is just private to the service. However, the falls apart if I refresh the page. So, my thought was to do something like this:

/*...snip...*/
this.isLoggedIn = function() { 
  var deferred = $q.defer()
    , self     = this
    ;

  function handleLoggedInStatus(status) {
    if (status) {
      self.authenticated = true;
      deferred.resolve();
    }
    else {
      deferred.reject();
    }
  }

  if (this.authenticated === null) {
    $http.get('/user')
      .success(function(response) {
        handleLoggedInStatus(response.success);
      });
  }
  else {
    handleLoggedInStatus(this.authenticated);
  }

  return deferred.promise;
};

And then in my controller I would do something like this:

$scope.isLoggedIn = sessionService.isLoggedIn;  

And in my template I would do:

...data-ng-show="isLoggedIn()"

However, doing that would result in the following error:

10 $digest() iterations reached. Aborting!

I tried a few different ways of referencing the sessionService.isLoggedIn function, such as:

$scope.isLoggedIn = sessionService.isLoggedIn();
$scope.isLoggedIn = sessionService.isLoggedIn.bind(sessionService)();
$scope.isLoggedIn = function() { return sessionService.isLoggedIn() }

But they either didn't work, or just gave me the same error.

Basically, I just want to be able to return a promise that will tell me whether or not the user is logged in. If we don't know if they are logged in (like after a page refresh), the promise will be resolved after an ajax request. If we do know already (like with normal navigation throughout the single page app) then the promise will be resolved immediately. I would then like to use that in my views so I can show/hide certain things, such as links to logout or view the account page.

What am I doing wrong?

like image 647
Scott Avatar asked May 03 '13 19:05

Scott


People also ask

What does Promise return in angular?

What Is Promise in Angular? Promises in Angular provide an easy way to execute asynchronous functions that use callbacks, while emitting and completing (resolving or rejecting) one value at a time. When using an Angular Promise, you are enabled to emit a single event from the API.

How do I return a Promise object?

resolve() method in JS returns a Promise object that is resolved with a given value. Any of the three things can happened: If the value is a promise then promise is returned. If the value has a “then” attached to the promise, then the returned promise will follow that “then” to till the final state.

Can we use Promise in angular?

Promises can be executed by calling the then() and catch() methods. The then() method takes two callback functions as parameters and is invoked when a promise is either resolved or rejected. The catch() method takes one callback function and is invoked when an error occurs.

How do you get a Promise value?

Define an async function. Use the await operator to await the promise. Assign the result of using the await operator to a variable.


1 Answers

You're resolving your promise, but not with a value--so the value of the promise on the $scope when resolved is undefined, which is falsy, thus your ng-show is not triggering.

It seems you're looking for something more like this:

In the service:

function handleLoggedInStatus(status) {
  if (status) {
    self.authenticated = true;
  }
  deferred.resolve(status); // always resolve, even if with falsy value
}

if (this.authenticated === null) {
  $http.get('/user')
    .success(function(response) {
      handleLoggedInStatus(response.success);
    })
    .error(function(data) {
      deferred.reject(data.errorMsg); // reject if there was an error
    });
} else {
  handleLoggedInStatus(this.authenticated);
}

In the controller:

$scope.loggedIn = sessionService.isLoggedIn();

In the HTML:

<div ng-show='loggedIn'>...</div>

Here is a JSFiddle demonstrating resolving the deferred with a truthy value and binding to the $scope.


Note that you can't bind the function itself to the scope

$scope.loggedIn = sessionService.isLoggedIn

and call the function in the view

<div ng-show="loggedIn()">...</div>

because the function returns a different promise each digest cycle (which is why you were getting the '10 digest cycles' error). You could, however, ensure that extra calls to sessionService.isLoggedIn returns the same promise instead of creating a new one, since you can call then on a promise multiple times (and in fact this is one of the benefits of promises):

deferred = null;

isLoggedIn: function() {
  if (!deferred) {
    deferred = $q.defer();
    $http.get('/user')
      .success(function(response) {
        deferred.resolve(response.success); // resolve if true or false
      })
      .error(function(data) {
        deferred.reject(data.errorMsg); // reject if there was an error
      });
  }
  return deferred.promise;
}

You could then get rid of the this.authenticated boolean, as you do not need to keep track of a previously-logged-in user across function calls (since the promise does this for you).

However, while this gets rid of the digest cycle error, you still cannot call the function from the view--I suspect Angular is treating the return value (the promise itself) as a truthy value, rather than binding to the promise's resolved value. Here's an example of it not working; notice the div is displayed even though the promise is resolving with false.


To use deferred.reject to indicate the user was not authenticated, as in your original service, you'd want to do something more like this in the controller, though I believe that resolveing with false is cleaner:

sessionService.isLoggedIn()
  .then(function() {
    $scope.loggedIn = true; // resolved
  }).then(function() {
    $scope.loggedIn = false; // rejected
  });
like image 82
Michelle Tilley Avatar answered Oct 14 '22 23:10

Michelle Tilley