Scoping issue in callback function in AngularJS controller

308 views Asked by At

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?

2

There are 2 answers

3
sampathsris On BEST ANSWER

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

/***************************
* THIS LINE IS THE CULPRIT
***************************/
$scope.$apply(function () {
    $scope.authStatus = status;
});
2
Ori Drori On

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);