7

I am trying to use ng-switch with ng-include below. The problem is with ng-init and the whole controller block getting re-rendered on any ng-includes change.

In the login_form.html, when a user logins, I set the isLoggedIn = true, in the LoginCtrl. However this causes the re-rendering of the full html below, which causes the ng-init again.

How do I avoid this cycle?

      <div ng-controller="LoginCtrl" ng-init="isLoggedIn = false" class="span4 pull-right">
        <div ng-switch on="isLoggedIn"> 
          <div ng-switch-when="false" ng-include src="'login_form.html'"></div>
          <div ng-switch-when="true" ng-include src="'profile_links.html'"></div>
        </div>
      </div>

Below is the HTML for the login form -

<form class="form-inline">
  <input type="text" placeholder="Email" ng-model="userEmail" class="input-small"/>
  <input type="password" placeholder="Password" ng-model="userPassword" class="input-small"/>
  <button type="submit" ng-click="login(userEmail, userPassword)" class="btn">Sign In</button>
</form>

Below is the controller -

angularApp.controller('LoginCtrl', function($scope, currentUser){

  $scope.loginStatus = function(){
    return currentUser.isLoggedIn();
  };

/*  $scope.$on('login', function(event, args) {
    $scope.userName = args.name;
  }); 

  $scope.$on('logout', function(event, args) {
    $scope.isLoggedIn = false;
  });*/

  $scope.login = function(email, password){
    currentUser.login(email, password);
  };

  $scope.logout = function(){
    currentUser.logout();
  };

});

Blow is the service -

angularApp.factory('currentUser', function($rootScope) {
  // Service logic
  // ...
  // 
    var allUsers = {"rob@gmail.com": {name: "Robert Patterson", role: "Admin", email: "rob@gmail.com", password: "rob"},
            "steve@gmail.com":{name: "Steve Sheldon", role: "User", email: "steve@gmail.com", password: "steve"}}

  var isUserLoggedIn = false;

  // Public API here
  return {
    login: function(email, password){
      var user = allUsers[email];
      var storedPass = user.password;

      if(storedPass === password){
        isUserLoggedIn = true;
        return true;
      }
      else
      {
        return false;
      }
    },
    logout: function(){
      $rootScope.$broadcast('logout');
      isUserLoggedIn = false;
    },

    isLoggedIn: function(){
      return isUserLoggedIn;
    }
 };
});
murtaza52
  • 46,887
  • 28
  • 84
  • 120

3 Answers3

39

I believe your problem is a result of the way prototypal inheritance works. ng-include creates its own child scope. Assigning a primitive value in a child scope creates a new property on that scope that shadows/hides the parent property.

I'm guessing that in login_form.html you do something like the following when a user logs in:

<a ng-click="isLoggedIn=true">login</a>

Before isLoggedIn is set to true, this is what your scopes look like:

before assignment

After isLoggedIn is set to true, this is what your scopes look like:

after assignment

Hopefully the pictures make it clear why this is causing you problems.

For more information about why prototypal inheritance works this way with primitives, please see What are the nuances of scope prototypal / prototypical inheritance in AngularJS?

As the above link explains, you have three solutions:

  1. define an object in the parent for your model, then reference a property of that object in the child: parentObj.isLoggedIn
  2. use $parent.isLoggedIn in login_form.html -- this will then reference the primitive in the $parent scope, rather than create a new one. E.g.,
    <a ng-click="$parent.isLoggedIn=true">login</a>
  3. define a function on the parent scope, and call it from the child -- e.g., setIsLoggedIn(). This will ensure the parent scope property is being set, not a child scope property.

Update: in reviewing your HTML, you may actually have two levels of child scopes, since ng-switch and ng-include each create their own scopes. So, the pictures would need a grandchild scope, but the three solutions are the same... except for #2, where you would need to use $parent.$parent.isLoggedIn -- ugly. So I suggest option 1 or 3.

Update2: @murtaza52 added some code to the question... Remove ng-init="isLoggedIn = false" from your controller (your service is managing the login state via its isUserLoggedIn variable) and switch on loginStatus() in your controller: <div ng-switch on="loginStatus()">.

Here is a working fiddle.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • I suppose my answer is solution 1? – asgoth Dec 31 '12 at 18:31
  • @asgoth, well, your fiddle uses essentially solution 1, although there is yet another child scope due to the additional controllers. Your "one controller" answer uses essentially solution 3 -- you are calling a function in the parent scope (which is available to child scopes due to prototypal inheritance) to change a parent scope property. But then there is a bit of solution 1 in there too, due to the parent object property -- you can use a primitive with solution 3. Here's a [fiddle for solution 3](http://jsfiddle.net/mrajcok/jNxyE/) that uses a primitive in the parent scope. – Mark Rajcok Dec 31 '12 at 20:27
  • +1 for the detailed answer Mark. Let me implement your solution and see if it resolves the problem. – murtaza52 Jan 03 '13 at 13:11
  • Mark I have added the code for the service and controller I have and also the login_form.html. Even after I sign in the loginStatus() still returns false. My hunch is this due to the var isUserLoggedIn being reset to false, in the service 'currentUser' – murtaza52 Jan 03 '13 at 14:27
  • @murtaza52, see Update2 in my answer. – Mark Rajcok Jan 03 '13 at 15:33
  • What happens when you have a directive with an isolated scope and the directive expects a primitive? Would `` also work? – Pablo Jun 30 '17 at 10:36
3

I've have a working example. The trick is that the scope variable to be evaluated has to be an object, not a primitive type. It looks like $scope.$watch() is not watching primitive types properly (which is a bug). The jsFiddle has a parent controller with two child controllers, but it would also work with only one (parent) controller.

HTML:

<div ng-controller="LoginCheckCtrl">
        <div ng-switch on="user.login"> 
          <div ng-switch-when="false" ng-include="'login'"></div>
          <div ng-switch-when="true" ng-include="'logout'"></div>
        </div>
</div>

Controller:

function LoginCheckCtrl($scope) {
    $scope.user = {
        login: false
    };
}

Note: this will also work with only one controller:

function LoginCheckCtrl($scope) {
    $scope.user = {
        login: false
    };
    $scope.login = function() {
        console.log($scope.user.login ? 'logged in' : 'not logged in');
        $scope.user.login = true;
    };
    $scope.logout = function() {
        console.log($scope.user.login ? 'logged in' : 'not logged in');
        $scope.user.login = false;
    };
}
asgoth
  • 35,552
  • 12
  • 89
  • 98
  • This also results in the same behavior. The re-rendering still resets the variable. Any ideas on how to prevent rerendering or reset ? – murtaza52 Dec 31 '12 at 11:57
  • Just saw, my jsfiddle went into an endless loop :) – asgoth Dec 31 '12 at 11:58
  • 1
    Edited my answer. Check the example in the link. – asgoth Dec 31 '12 at 12:36
  • +1 for your comment and +1 for answer. The solution you have suggested depends upon creating separate controllers for each control. I think that is the better approach, and I will use it. However any idea on why any variables in the controller are reset ? – murtaza52 Dec 31 '12 at 13:59
  • Each include has its own controller, but they inherit the parent. So, $scope.user is only defined on the parent. The variables are reset because they are primitive types, not because they are in the same controller. I just tested it, by removing the child controllers (and defining the function on the parent). – asgoth Dec 31 '12 at 14:03
  • "any idea on why any variables in the controller are reset ?" -- most likely they are not getting reset. I believe a new isLoggedIn property is being created on a child scope that is hiding/shadowing the parent property of the same name. See my answer for more details. – Mark Rajcok Dec 31 '12 at 17:23
  • I have updated my post with the complete code. Also please check the comment I left for Mark under his answer. – murtaza52 Jan 03 '13 at 14:28
0

You could store the state that needs to survive the reinitialization of the controller in a parent scope. I don't think it's strange to put isLoggedIn on the $rootScope even.

Also, you could initialize inside the controller, that would be cleaner in this case (but it doesn't solve your problem).

iwein
  • 25,788
  • 10
  • 70
  • 111