0

I have the following angular app (JSFiddle):

HTML

<form data-ng-app="jsApDemo" data-ng-controller="loginCtrl">
    <label for="username">Username:</label>
    <input type="text" id="username" data-ng-model="username" />
    <br />
    <label for="password">Password:</label>
    <input type="password" id="password" data-ng-model="password" />
    <br />
    <button type="submit" data-ng-click="executeLogIn()">Log in</button>
    <br />
    <label for="authstatus">Authentication status:</label>
    <input type="text" id="authstatus" readonly value="{{ authStatus }}" />
</form>

It is a simple login form, and when the user clicks submit, I want to execute a function in controller loginCtrl. loginCtrl calls a service that makes the actual authentication process.

JavaScript

// the controller and its module
angular.module('jsApDemo', ['Core'])
.controller('loginCtrl', ['$scope', 'Connection', function ($scope, Connection) {
    $scope.authStatus = 'unauthenticated';

    $scope.executeLogIn = function () {
        $scope.authStatus = 'authenticating';

        Connection.sessionInitialize({
            username: $scope.username,
            password: $scope.password
        }, function (error, status) {
            if (!error) {
                /***************************
                 * THIS LINE IS THE CULPRIT
                 ***************************/
                $scope.authStatus = status;
            }
        });
    };
}]);

// the service and its module
angular.module('Core', []).service('Connection', function () {
    this.sessionInitialize = function (options, callback) {
        if (!options || !options.username || !options.password) {
            callback('Username, and password are mandatory', null);
            return;
        }

        setTimeout(function () {
            callback(null, 'authenticated');
        }, 1000);
    };
});

In service Connection, I have used a setTimeout (Note: setTimeout was used as a placeholder for an asynchronous call. My original code did not have a setTimeout. It had a call to an asynchronous function in a third party library. I could not make the JSFiddle with that call included in the code. So I replaced the call to that library with a setTimeout to demonstrate the asynchronous nature of the code).

Problem arises when I try to access $scope from within the callback function to Connection.sessionInitialize. After debugging I found out that following line does not work:

/***************************
* THIS LINE IS THE CULPRIT
***************************/
$scope.authStatus = status;

It seems like a scoping issue, but a simple console.log($scope) statement prior to this line shows that $scope has the correct value. However the textbox #authstatus which has its value attribute bound to $scope.authStatus does not change.

What am I doing wrong?

sampathsris
  • 21,564
  • 12
  • 71
  • 98
  • 2
    You need to set `$scope` properties inside `$scope.$apply(function(){ ... })`, read https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply Because you have set `authStatus` property outside of angular – Rahil Wazir Aug 09 '15 at 09:11
  • Thank you @RahilWazir: But could you explain a bit more? I'm not sure how I can use `$scope.$apply` in this instance. – sampathsris Aug 09 '15 at 09:17
  • 1
    If you use [$timeout](https://docs.angularjs.org/api/ng/service/$timeout) instead of setTimeout then you won't need to call $apply. – Gruff Bunny Aug 09 '15 at 09:22
  • @GruffBunny: Use of `setTimeout` is just for demo puposes. It will actually be a web request. But thanks for the hint on `$timeout`. – sampathsris Aug 09 '15 at 09:25

2 Answers2

0

The setTimeout is the culprit, as it runs the callback, updates the scope, but doesn't run the digest cycle, that make the two way data binding to work. Use $timeout service instead:

$timeout(function () {
    callback(null, 'authenticated');
}, 1000);
Rahil Wazir
  • 10,007
  • 11
  • 42
  • 64
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Thank you for the answer, but `setTimeout` was used as a placeholder for an asynchronous call. My original code did not have a `setTimeout`. It had a call to an asynchronous function in a third party library. I could not make the JSFiddle with that call included in the code. So I replaced the call to that library with a `setTimeout` to demonstrate the asynchronous nature of the code. So in this instance `$timeout` is not a solution for me. Sorry for the confusion. – sampathsris Aug 10 '15 at 10:09
  • 1
    No worries. It's not my -1 :) btw - move the $apply to wrap the callback in the service, and use $rootScope.$apply. In this way, you won't have to use $scope.$apply in every controller that uses this service. – Ori Drori Aug 10 '15 at 10:24
-1

Thanks to @RahilWazir, I came up with a solution:

/***************************
* THIS LINE IS THE CULPRIT
***************************/
$scope.$apply(function () {
    $scope.authStatus = status;
});
sampathsris
  • 21,564
  • 12
  • 71
  • 98
  • @RahilWazir: It is not (at least for me). because the `setTimeout` was used as a placeholder for an asynchronous call. It's there just to show the asynchronous nature of the code. So (at least for me), It does not work. – sampathsris Aug 10 '15 at 09:55
  • Define _does not work_? Have you injected the `$timeout` service? It has the same use as `setTimeout` – Rahil Wazir Aug 10 '15 at 09:59
  • @RahilWazir: My original code did not have a `setTimeout`. It had a call to an asynchronous function in a third party library. I could not make the JSFiddle with that call included in the code. So I replaced the call to that library with a `setTimeout` to demonstrate the asynchronous nature of the code. So in this instance `$timeout` is useless for me, because what I want is not a timeout. I hope this explanation is clear? – sampathsris Aug 10 '15 at 10:05