Waiting for $rootScope value to resolve in Angular before page load
Asked Answered
S

3

6

So I'm running into this problem where I'm using ngView and I have a navigation bar that is static throughout like so:

<div ng-include="'views/nav.html'" ng-controller="NavCtrl"></div>
<div class="container-fluid" ng-view=""></div>

This nav.html, the navigation bar, displays a certain set of functions (Login, Register) if the user is logged out (using ng-show) and other menu options if the user is logged in. Because of the heavy use of current user, I've put this information in the $rootScope like this: $rootScope.currentUser - returns user object, and $rootScope.signedIn - return boolean.

Basically, I want to delay the navbar from loading until $rootScope.signedIn is loaded and either true or false, and $rootScope.currentUser is an object or undefined.

I've tried messing around with creating promises in my app.config routes, but I'm not sure how I can return a promise to the permanent view state.

Any help is appreciated.

Edit:

Here is the service in which I broadcast my login. This fires anytime a user is authenticated/logged in or anytime they logout:

    var authClient = new FirebaseSimpleLogin(refDownload, function(error, user) {
        if (error) {
            incorrectLogin(error.code);
        }
        if (user) {
            // user authenticated
            $rootScope.$broadcast('login');
            correctLogin(user.id);
        } else {
            // user is logged out
            $rootScope.$broadcast('logout');
        }
    });

This service is injected into the NavCtrl controller in the following way:

    $scope.isHidden = true;

    $scope.$on('login', function() {
        console.log('login broadcast');
        $scope.isHidden = false;
    });

    $scope.$on('logout', function() {
        console.log('broadcast logout');
        $scope.isHidden = true;
    });

The template for this controller is nav.html that looks like this:

<div class="col-xs-4 centered" id="nav-hover"  ng-show="isHidden">
    <ul class="nav navbar-nav">
        <li id="nav-login"><a ng-href="#/login"><span class="glyphicon glyphicon-log-in">&nbsp;Login</span></a></li>
    </ul>
</div>

<div class="col-xs-4 centered" id="nav-hover" ng-show="isHidden">
    <ul class="nav navbar-nav">
        <li id="nav-login"><a ng-href="#/register"><span class="glyphicon glyphicon-edit">&nbsp;Register</span></a></li>
    </ul>
</div>


<div class="col-xs-2 centered" id="nav-hover">
    <ul class="nav navbar-nav" ng-hide="isHidden">
        <li ng-class="{{ chatCat.active }}"><a ng-href="{{ chatCat.url }}"><span class="{{ chatCat.icon }}"></span></a></li>
    </ul>
</div>

Again, this view is bound to NavCtrl. When logging users in, I use AuthCtrl as follows:

    $scope.login = function() {
        if ($scope.user !== undefined) {
            Auth.login($scope.user);
            $location.path('/dexter');
        } else {
            console.log('nothing entered');
        }               
    };

When I try to login, the nav view does not update with the new values, although the broadcast is fired from the service with 'logged in'.

Auth service:

'use strict';

app.factory('Auth',
    function($rootScope, $location, $firebase, $firebaseSimpleLogin, firebaseUrl) {

    var refDownload = new Firebase(firebaseUrl + 'housemates');

    var sync = $firebase(refDownload); 

    var ref = sync.$asObject();

    var authClient = new FirebaseSimpleLogin(refDownload, function(error, user) {
        if (error) {
            incorrectLogin(error.code);
        }
        if (user) {
            // 1
            // user authenticated
            correctLogin(user.id);
        } else {
            // user is logged out
            // $rootScope.signedIn = false;
        }
    });

    var Auth = {

        housemates: ref,

        changeColor: function(color) {
            var id = $rootScope.currentUser.id.toString();
            refDownload.child(id).update({ color: color });
            $rootScope.currentUser.color = color;
        },


        create: function(authUser, usr) {
            refDownload.child(authUser.id).set({
                initials: usr.initials,
                email: authUser.email,
                password: usr.password,
                color: 'Blue',
                id: authUser.id,
                uid: authUser.uid,
                rememberMe: true,
            });

        },

        // 3
        findById: function(id) {
            refDownload.on('value', function(snapshot) {
                var userObject = snapshot.val();
                // 4 - sets currentUser
                //$rootScope.currentUser = userObject[id];
                var currentUser = userObject[id];
                Auth.setUser(currentUser);
                // $rootScope.signedIn = true;
            }, function (error) {
                console.log(error);
            });
        },

        login: function(user) {
            authClient.login('password', {
                email: user.email,
                password: user.password,
                rememberMe: true
            });
        },

        logout: function() {
            delete $rootScope.currentUser;
            delete $rootScope.signedIn;
            delete $rootScope.error;
            return authClient.logout();
        },

        register: function(user) {
            var userSimple = user;
            authClient.createUser(user.email, user.password, function(error, user) {
                if(!error) {
                    var userComplex = user;
                    Auth.login(userSimple);
                    Auth.create(userComplex, userSimple);
                    return user;
                } else {
                    console.log(error);
                }
            });

        },

        setUser: function(aUser) {
            console.log('setuser ran');
            $rootScope.currentUser = aUser;
            console.log('setUser: ' + $rootScope.currentUser);
        },

        isLoggedIn: function() {
            console.log($rootScope.currentUser);
            return ($rootScope.currentUser) ? $rootScope.currentUser : false;
        },


    };

    // 2
    function correctLogin(id) {
        Auth.findById(id);
    }

    function incorrectLogin(error) {
        alert(error);
        $rootScope.error = error;
    }

    return Auth;


});
Scissel answered 3/10, 2014 at 14:7 Comment(12)
Maybe the ngCloak could help you. Or a $timeout maybe...Devries
I tried putting ng-cloak on the top div in the nav.html view, but it doesn't fix the problem.Scissel
You should also try a service instead of $rootScope.Faires
I started out using a service for my current user details, but I'm calling my user object information on literally every page and on the views - isn't that use case for $rootScope? Also, this doesn't address my problem.Scissel
There's no use case for $rootScope except broadcasts maybe, and you should avoid those too. A service is persistent (sort of a singleton) so there's no harm in $scope.foo = someService.getSomeData();.Faires
Also, to address the issue, only you know what loads $rootScope.signedIn. In that code you could also $rootScope.userLoaded = false; __load the data__.onSuccessDo: $rootScope.userLoaded = true; and then ng-show="userLoaded" on the navigation bar.Faires
Thank you for the tip, I will work on converting my $rootScope objects to service objects from which they came. Do you have any advice about my current predicament?Scissel
@Scissel read the docs for ng-cloak, to make it work properly in all cases you need to do some stuff w/CSS so that the element you use it with can be hidden as soon as possible on page load. Another approach would be to take a look at Angular UI Router -- it's like ngView on steroids. It has a nice "resolve" feature which will not show the view until your promise is resolved.Celeski
@SergiuParaschiv My $rootScope.signedIn functionally return true or false. So I am running ng-show="signedIn" in my view. My problem is that $rootScope.signedIn is false, or undefined, until the service loads the data. So it briefly shows the navbar with the "false" ng-show value until the user clicks something.Scissel
@SunilD. I'll take a look at UI Router for future use, but as I'm currently using ngView, I'm not looking to do an overhaul quite yet.Scissel
Seems OK at first glance (though I have never used firebase), but to be sure, pls put {{isHidden}} somewhere in the nav.html and turn the chrome developer console on to see if some error has broken the javascript code.Drucilla
I put {{ isHidden }} in the Nav and logging in does not seem to change the value. However, when I click an item on the navbar, then isHidden value updates. I seems to me to be a controller issue. In my index.html I call the the nav view and the nav ctrl. But when the login page opens, it uses AuthCtrl rather than NavCtrl. Could this be the issue? Can I wrap my divs in index.html around the ng-include? How can I change the isHidden value in nav scope from other controllers? Shouldn't it automatically change with the logins/logouts from the service?Scissel
D
7

With a bit of $rootScope.$broadcast and ng-hide on the menu, this could be easily accomplished. See this plunker

The html:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="[email protected]" src="https://code.angularjs.org/1.2.25/angular.js" data-semver="1.2.25"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
  <div ng-include="'nav.html'" ng-controller="NavCtrl" ng-hide="isHidden"></div>
  <button class="btn" ng-click="login()">Login</button>
  <button class="btn" ng-click="logout()">Logout</button>
  </body>

</html>

The javascript:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope, $rootScope) {
  $scope.login = function() {
    $rootScope.$broadcast("login");
  }

  $scope.logout = function() {
    $rootScope.$broadcast("logout");
  }
});

app.controller('NavCtrl', function($scope) {
  $scope.isHidden = true;
  $scope.$on('login', function() {
    console.log("logged in");
    $scope.isHidden = false;
  });

  $scope.$on('logout', function() {
    console.log("logged out");
    $scope.isHidden = true;
  });
});
Drucilla answered 3/10, 2014 at 15:7 Comment(10)
I've successfully broadcasted the login/logout event to the NavCtrl controller, however, on successful user sign-in, the navbar doesn't change until the user clicks something.Scissel
What do you mean by "the navbar doesn't change until the user clicks something"? As I understood your question, you want to show or hide the navbar according to the login status of the user.Drucilla
The broadcast seems to register, because NavCtrl console.logs "logged in", but the view doesn't update for some reason when the user logs in... the user has to click something before the navbar view changesScissel
Have you tried my plunker sample? Is this the behaviour you want?Drucilla
This is the behavior that I wanted. However, the broadcast is only changing the view/setting the state on page load, and not when the user logs in as an instantaneous change.Scissel
Meaning, the navbar reflects the changes or "gets" the state when I load up the app correctly, and the broadcast fires, but when I login, the broadcast fires, but the state doesn't change.Scissel
I don't know how exactly you handle your login process, but it should follow the code in the sample I wrote. When the user is authenticated broadcast a "login" event and in the controller where the parent view of the navbar is have an "on" event that waits for the "login" event to be sent. Then set a variable ("hidden") to false and hook it up to the ng-hide directive on the navbar tag.Drucilla
Your example was very helpful, I implemented it, and it worked as expected. On user authentication, the service broadcasts 'login', the scope picks this up, and logs 'logged in' to the console. So any time a user is authenticated, or logged out, or logged in or whatever, the broadcast event fires as expected. But when I log in manually, the view doesn't updated with the 'isHidden'. It only registers after the user clicks something else.Scissel
Maybe you can provide some code sample. I am sure this will help. I guess you have a timing problem. Maybe you are authenticating against a backend service via a http call and this takes a while so you have to use promises to know when the call has finished.Drucilla
Please see the update above, I provided examples of the service that gets the data, the $broadcast in the service, the controller that it is injected into, and the view. I also showed the controller that's linked to the Login template view.Scissel
D
1

OK, if the way i suggested isn't working for you, here is a second possible solution (plunker)

The base idea is to have a service (in this case a factory) in which you set the logged in user name and then in the nav controller use $watch to watch changes to the authentication status in the service. And the code:

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="[email protected]" src="https://code.angularjs.org/1.2.25/angular.js" data-semver="1.2.25"></script>
    <script src="app.js"></script>
    <script src="Auth.js"></script>
  </head>

  <body ng-controller="MainCtrl">
  <div ng-include="'nav.html'" ng-controller="NavCtrl" ng-hide="isHidden"></div>
  <button class="btn" ng-click="login()">Login</button>
  <button class="btn" ng-click="logout()">Logout</button>
  </body>

</html>

The javascript:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope, $rootScope, Auth) {
  $scope.login = function() {
    var user = "iris"
    Auth.setUser(user);
  }

  $scope.logout = function() {
    Auth.setUser(null);
  }
});

app.controller('NavCtrl', function($scope, Auth) {
  $scope.isHidden = true;

  $scope.$watch(Auth.isLoggedIn, function (value, oldValue) {

    console.log("authentication changed");

    if(!value && oldValue) {
      console.log("logged out");
      $scope.isHidden = true;
    }

    if(value) {
      console.log("logged in");
      $scope.isHidden = false;
    }

  }, true);


});

and the service:

app.factory('Auth', function() {
  var user;

  return {
    setUser: function(aUser) {
      user = aUser;
    },
    isLoggedIn: function() {
      console.log(user);
      return (user) ? user : false;
    }
  }
})
Drucilla answered 3/10, 2014 at 19:31 Comment(2)
Hmm, I tried adding the isLoggedIn function along with the controller $watch, but my Auth service is more complex than immediately setting the user as current user. It has to authenticate, then find the user by their Id number. I'll post entire factory above.Scissel
Well I suspected there is some complexity in your authentication code that prevents the code from working. If you have another dependent step (as finding a user by id) you need to use promises. Anyway, I'm glad you solved your problem.Drucilla
S
0

@zszep $broadcast answer solved the problem with one caveat. It was necessary to add $scope.$apply() following each of the $scope.isHidden commands in NavCtrl. This forced a page refresh of sorts and the Nav view updated.

Scissel answered 3/10, 2014 at 23:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.