Navigation

The code:

To create a directory named MeanNav with this project code in it, execute the following, then follow the directions in the MeanSeed chapter to set up that directory.

git clone -b MeanNav https://github.com/amnotafraid/MeanSeed.git MeanNav

Topics: Angular controllers, directives, and services.

The MeanSeed app has a simple navigation bar that looks like this:

If you sign up as a user, the navigation bar stays the same.

What I really want is a navigation bar that "knows" whether a person is logged in or not. In the future, I want a navigation bar that changes according to whether a user is logged in and what kind of role the user is in.

To do this, I am going to move the navigation bar code to a directive. The directive will be associated with a controller. While I'm at it, I will add a service that will be the bare bones to authenticate and authorize a user. What are directives, controllers, and services?

There is a thorough definition of directives here. In part, it explains that a directive is a function that executes when the Angular compiler finds it in the DOM. Another way to put it, directives are JavaScript functions that manipulate and add behaviors to HTML DOM elements. A directive has a name which can be used to extend HTML. I am going to make a directive called top-nav that will put the navigation bar in the HTML when it sees this:

<top-nav></top-nav>

A controller is what manages getting data from the database to the web page and vice-versa. It a JavaScript object with variables and functions that connect the model to the view.

A service centralizes functionality and data so that the entire application can use it. Services provide a means for keeping data around for the lifetime of an application that can be used by different many different controllers.

top-nav directive

The reason this is going to be called top-nav instead of nav is because

I made a client/app/scripts/directives directory and in a file in there named topNav.js, place the following code:

'use strict';

angular.module('clientApp')
  .directive('topNav', function () {
    return {
      templateUrl: '/views/top-nav.html' ,
      restrict: 'E',
      controller: 'NavCtrl'
    };  
  });

So, this directive simply returns an object with some parameters set. Here templateUrl points to a file where the html for the directive is found. The restrict property shows that this directive will only match an 'E' element, like this: <top-nav></top-nav>, not like an 'A' attribute, like this: <div top-nav></div>. The controller names the controller.

By the way, AngularJS "normalizes" the name used in HTML. data_ and x- are stripped, and the remainder is camelcased with :, - and _ as word boundaries. This makes top-nav in HTML become topNav in JavaScript.

I need to make the top-nav.html controller and the nav.js files.

top-nav.html

The client/app/index.html file has some code to handle the navigation:

<div class="navbar navbar-default" role="navigation">
  <div class="container">
    <div class="navbar-header">
      ... 
    </div>
  </div>
</div>

I am going to pull that code out of there, change it a little and put it in client/app/views/top-nav.html:

<nav class="navbar navbar-default" role="navigation">
  <div class="container">
    <div class="navbar-header">

      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>

      <a class="navbar-brand" href="#/">home</a>
    </div>
    <div class="collapse navbar-collapse pull-right" id="js-navbar-collapse">
      <ul class="nav navbar-nav">
        <li ng-show="isLoggedIn()"><a>{{ currentUser() }}</a></li>
        <li ng-show="isLoggedIn()"><a href="" ng-click="logOut()">Log Out</a></li>
        <li ng-hide="isLoggedIn()"><a href="" ng-click="logIn()">Log In</a></li>
        <li ng-hide="isLoggedIn()"><a href="/#/signup">Sign Up</a></li>
      </ul>
    </div>
  </div>
</nav>

This is classic bootstrap navigation that collapses down to a hamburger if the screen is small: , pulls the menu to the right, and adds a link to home on the left.

This code is associated with the NavCtrl controller in the directive.

Initially, I also added the ng-contoller directive like this:

<nav class="navbar navbar-default" role="navigation" ng-controller="NavCtrl">

However, as this stackoverflow answer shows, this makes the controller execute twice.

This code uses the ng-show and ng-hide Angular directives to control whether some menu items appear. There are also some ng-clicks in there to call logIn() and logOut() functions. {{ currentUser() }} is an Angular expression, in this case a value returned by a function. All these functions:

  • isLoggedIn()
  • currentUser()
  • logOut()
  • logIn() are in the nav.js controller. However, those functions depend heavily on the authentication service. So, that's what I will talk about next.

authentication service, authService.js

There is a useful article on AngularJS services here. There are three ways to create a service in Angular: a factory, a service, and a provider. I am going to create the file client/app/scripts/services/authService.js. For authService.js, I am going to use a factory. When you’re using a Factory you create an object, add properties to it, then return that same object. When you pass this service into your controller, those properties on the object will now be available in that controller through your factory.

Here is the skeleton of creating a factory:

'use strict';

function Auth() {
  var _this = this;
}

angular.module('clientApp')
  .factory('auth', [
    function () {
      return new Auth();
    }
  ]);

Inside the Auth() function, I need to add these properties:

  • user - this is a JSON object with firstName and lastName properties
  • fLoggedIn - this is a boolean value that tracks whether a user is logged in or not.

And these functions:

  • currentUser() - if a user is logged in, this is going to return their name.
  • isLoggedIn() - this will return true or false depending on whether a user is logged in.
  • logIn(user) - this will log user in
  • logOut() - this will log out a user

It will look like this:

function Auth() {
  var _this = this;
  this.fLoggedIn = false;
  this.user = {};

  this.currentUser = function () {
    if (_this.isLoggedIn ()) {
      return _this.user.firstName + ' ' + _this.user.lastName;
    }
  };

  this.isLoggedIn = function () {
    return _this.fLoggedIn;
  };

  this.logIn = function(user){
    _this.user = user;
    _this.fLoggedIn = true;
  };

  this.logOut = function () {
    delete _this.user;
    _this.fLoggedIn = false;
  };
}

If you look at the code, it doesn't do much. When I add the authentication, I'll fix it so that it actually checks the backend to make sure the user enters a correct password.

By the way, the reason _this is used is because once you get inside a function, this refers to the function. By declaring _this, you always have it around to use inside a child.

Now, I can tell you about the nav.js controller which will depend on the authService.

I am going to create a client\app\scripts\controllers\nav.js file, which is going to be a controller for the top-nav directive. This is the skeleton for creating a controller:

'use strict';

angular.module('clientApp')
  .controller ('NavCtrl', [
    '$scope',
    function ($scope) {
    }
  ]);

This controller is called NavCtrl because that is what we identified it as in the directive: controller: 'NavCtrl'.

Every controller has a $scope that gets passed in. According to the AngularJS documentation:

Scope is the glue between application controller and the view. During the template linking phase the directives set up $watch expressions on the scope. The $watch allows the directives to be notified of property changes, which allows the directive to render the updated value to the DOM.

If you make a property or function on the $scope, the view is going to have access to it.

This controller is going to need to use the auth service that we created, so we 'inject' it like this:

angular.module('clientApp')
  .controller ('NavCtrl', [
    'auth',
    '$scope',
    function (auth, $scope) {
    }
  ]);

Adding auth is going to make everything in the authService.js factory available. The reason it's called auth is because of the way it's defined in authService.js:

angular.module('clientApp')
  .factory('auth', [
  ...

Later on, I will make a form where the user can enter their username and password to login. I will change the authService to send the login information to the server side for authentication. The server will pass back a token that will contain the user's first and last name, so the nav bar can use the user's name.

For right now, just to get things rolling, I will hard code a user named Bugs Bunny. What the NavCtrl does is make the functionality of auth available to the top-nav directive. It looks like this:

'use strict';

angular.module('clientApp')
  .controller ('NavCtrl', [
    'auth',
    '$scope',
    function (auth, $scope) {
      $scope.user = { 
        firstName: 'Bugs',
        lastName: 'Bunny'
      };  

      $scope.isLoggedIn = auth.isLoggedIn;

      $scope.currentUser = auth.currentUser;

      $scope.logOut = function () {
        auth.logOut();
      }

      $scope.logIn = function () {
        auth.logIn($scope.user);
      };  
    }   
  ]);

I had to do logOut and logIn differently from isLoggedIn and currentUser. At first, I had it like this:

$scope.logOut = auth.logOut();

$scope.logIn = auth.logIn(user);

But, as the HTML page is compiled by AngularJS, the directive controllers get executed, so auth.logOut() and auth.logIn() were getting executed, which wasn't exactly what I wanted. Wrapping them in an anonymous function fixed it so they could be called by the view, but prevented them from being executed during the compile stage.

index.html

There's only two things I still need to do, both of them in client\app\index.html file.

First of all, I need to add the top-nav directive that I made, so that the navigation will appear on the page:

    <div class="header">
      <top-nav></top-nav>
    </div>

Second, I need to include the files for the directive, controller, and service that I made. I addded the lines with the pluses:

         <script src="scripts/app.js"></script>
         <script src="scripts/controllers/main.js"></script>
+        <script src="scripts/controllers/nav.js"></script>
         <script src="scripts/controllers/about.js"></script>
         <script src="scripts/controllers/signup.js"></script>

+        <script src="scripts/directives/topNav.js"></script>

+        <script src="scripts/services/authService.js"></script>

locationProvider and the mysterious #!

I was used to seeing a # in my angular routes, like this:

http://localhost:3000/#

but what I was getting was this:

http://localhost:3000/#!

I don't know why I was getting that, but it wasn't just a silly semantic thing. Code like this: <a href="#/signup" class="btn btn-lg btn-success"> wasn't working.

I fixed it by changing client/app/scripts/app.js from this:

  .config([
    '$stateProvider',
    '$urlRouterProvider',
    function ($stateProvider, $urlRouterProvider) {

to this:

  .config([
    '$locationProvider',
    '$stateProvider',
    '$urlRouterProvider',
    function ($locationProvider, $stateProvider, $urlRouterProvider) {
    $locationProvider.hashPrefix("");

Conclusion

After adding these changes, the nav bar looks like this before the user is 'logged in':

And like this after 'Bugs Bunny' logs in:

On the left, there is always a link to get you back to the home page:

I added or changed these files:

    modified:   client/app/index.html
    modified:   client/app/scripts/app.js
    new file:   client/app/scripts/controllers/nav.js
    new file:   client/app/scripts/directives/topNav.js
    new file:   client/app/scripts/services/authService.js
    new file:   client/app/views/top-nav.html

results matching ""

    No results matching ""