Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ng-include causes the controller block to re render

Tags:

angularjs

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 = {"[email protected]": {name: "Robert Patterson", role: "Admin", email: "[email protected]", password: "rob"},
            "[email protected]":{name: "Steve Sheldon", role: "User", email: "[email protected]", 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;
    }
 };
});
like image 412
murtaza52 Avatar asked Dec 31 '12 11:12

murtaza52


2 Answers

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.

like image 186
Mark Rajcok Avatar answered Sep 27 '22 19:09

Mark Rajcok


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;
    };
}
like image 26
asgoth Avatar answered Sep 27 '22 18:09

asgoth