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;


});
Himmel
  • 3,629
  • 6
  • 40
  • 77
  • Maybe the [ngCloak](https://docs.angularjs.org/api/ng/directive/ngCloak) could help you. Or a $timeout maybe... – Denis C de Azevedo Oct 03 '14 at 14:11
  • I tried putting ng-cloak on the top div in the nav.html view, but it doesn't fix the problem. – Himmel Oct 03 '14 at 14:14
  • 1
    You should also try a service instead of `$rootScope`. – Sergiu Paraschiv Oct 03 '14 at 14:15
  • 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. – Himmel Oct 03 '14 at 14:18
  • 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();`. – Sergiu Paraschiv Oct 03 '14 at 14:25
  • 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. – Sergiu Paraschiv Oct 03 '14 at 14:26
  • 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? – Himmel Oct 03 '14 at 14:27
  • @Himmel 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. – Sunil D. Oct 03 '14 at 14:28
  • @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. – Himmel Oct 03 '14 at 14:34
  • @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. – Himmel Oct 03 '14 at 14:39
  • 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. – zszep Oct 03 '14 at 18:52
  • 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? – Himmel Oct 03 '14 at 19:03

3 Answers3

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="angular.js@1.2.x" 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;
  });
});
zszep
  • 4,450
  • 4
  • 38
  • 58
  • 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. – Himmel Oct 03 '14 at 16:51
  • 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. – zszep Oct 03 '14 at 16:59
  • 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 changes – Himmel Oct 03 '14 at 17:01
  • Have you tried my plunker sample? Is this the behaviour you want? – zszep Oct 03 '14 at 17:02
  • 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. – Himmel Oct 03 '14 at 17:08
  • 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. – Himmel Oct 03 '14 at 17:12
  • 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. – zszep Oct 03 '14 at 17:13
  • 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. – Himmel Oct 03 '14 at 17:16
  • 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. – zszep Oct 03 '14 at 17:23
  • 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. – Himmel Oct 03 '14 at 18:28
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="angular.js@1.2.x" 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;
    }
  }
})
zszep
  • 4,450
  • 4
  • 38
  • 58
  • 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. – Himmel Oct 03 '14 at 22:03
  • 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. – zszep Oct 04 '14 at 06:05
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.

Himmel
  • 3,629
  • 6
  • 40
  • 77