Angular.JS Routing Demo

From Logic Wiki
Jump to: navigation, search


Let's add some routing to our application:

  • First of all, we need to include Angular's ngRoute module, which I'm going to pull down from Cloudflare
  • Next, let's add that container div to our landing page, inside the body tag, before our script tags
  • Finally, we will add a couple of links to our landing page, which, when clicked will update the route and load the appropriate view into the container div
<!DOCTYPE html>
<html ng-app="AwesomeAngularMVCApp" ng-controller="LandingPageController">
<head>
    <title ng-bind="models.helloAngular"></title>
</head>
<body>
    <h1>{{models.helloAngular}}</h1>

    <ul>
        <li><a href="/#/routeOne">Route One</a></li>
        <li><a href="/#/routeTwo">Route Two</a></li>
        <li><a href="/#/routeThree">Route Three</a></li>
    </ul>

    <div ng-view></div>

    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-route.min.js"></script>
    @Scripts.Render("~/bundles/AwesomeAngularMVCApp")
    </body>
</html>


Now we want to tell our AngularJS application about these routes. We do this in the application module's config function (inside the same Javascript file in which we declared the application, at the root of our Scripts directory), mine now looks like this:

var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ngRoute']);

AwesomeAngularMVCApp.controller('LandingPageController', LandingPageController);

var configFunction = function ($routeProvider) {
    $routeProvider.
        when('/routeOne', {
            templateUrl: 'routesDemo/one'
        })
        .when('/routeTwo', {
            templateUrl: 'routesDemo/two'
        })
        .when('/routeThree', {
            templateUrl: 'routesDemo/three'
        });
}
configFunction.$inject = ['$routeProvider'];

AwesomeAngularMVCApp.config(configFunction);

So far so good, but these views don't exist yet. Let's fix that, create a C# controller inside the MVC controller directory called RoutesDemoController, and add three Action methods called One, Two and Three, like so:

using System.Web.Mvc;

namespace AwesomeAngularMVCApp.Controllers
{
    public class RoutesDemoController : Controller
    {
        public ActionResult One()
        {
            return View();
        }

        public ActionResult Two()
        {
            return View();
        }

        public ActionResult Three()
        {
            return View();
        }
    }
}

Right click on each instance of return View(); and select Add View..., tick Create as a partial view on the following dialog box leaving everything else as-is and click Add.

'Route two' takes parameters Let's modify route two to take a parameter, which will be passed all the way from angular through our MVC controller, going to our MVC view. Update the C# action method like so:

public ActionResult Two(int donuts = 1)
{
    ViewBag.Donuts = donuts;
    return View();
}

Now update the view itself so it looks like this:

Route two

@for (var i = 0; i < ViewBag.Donuts; i++)
{
    <p>Mmm, donuts...</p>

Now we need to tell angular about this parameter. Modify the config function in AwesomeAngularMVCApp.js like so:

var configFunction = function ($routeProvider) {
    $routeProvider.
        when('/routeOne', {
            templateUrl: 'routesDemo/one'
        })
        .when('/routeTwo/:donuts', {
            templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
        })
        .when('/routeThree', {
            templateUrl: 'routesDemo/three'
        });
}

And the route two hyperlink in our landing page could look like this:

<li><a href="/#/routeTwo/6">Route Two</a></li>


'Route three' is for registered users only The way things are right now, anybody on the internet can visit our site and see the contents of routes one two and three. Epic, but route three is going to contain some sensitive data, how do we protect that?

Using ASP.NET authentication of course:

Add the Authorize attribute to Route three's action method, like so:

[Authorize]
public ActionResult Three()
{
    return View();
}

If we run our app at this point and try to navigate to route three, we will get this epicness in our browsers developer console.

Failed to load resource: The server responded with a status of 404 (Not Found)... 

This actually means that everything is working as expected so far, but before we go any further with route three, let's get some Authentication on the go.

Authentication

At the moment, ASP.NET is detecting that the user doesn't have permission to access 'Route 3' and trying to redirect to "/Account/Login", directing us away from our landing page. What we really want is to deal with this 401 response ourselves in Angular. You do this with an interceptor.

Let's do this now. The first step is stopping ASP.NET MVC from redirecting on a 401, edit App_Start => Startup.Auth.cs. Look for the following:

LoginPath = new PathString("/Account/Login")

And replace it with this:

LoginPath = new PathString(string.Empty)

Now when we try to access 'Route 3' we get the following error in the browsers developer console:

401 Anauthorized

Much better. Let's handle this with an interceptor. Create a directory inside Scripts called Factories, and update the ScriptBundle inside BundleConfig to include this directory:

bundles.Add(new ScriptBundle("~/bundles/AwesomeAngularMVCApp")
   .IncludeDirectory("~/Scripts/Controllers", "*.js")
   .IncludeDirectory("~/Scripts/Factories", "*.js")
   .Include("~/Scripts/AwesomeAngularMVCApp.js"));

Create a new Javascript file inside Scripts => Factories called AuthHttpResponseInterceptor.js containing the following Angular factory (borrowed from the awesome SparkTree blog):

var AuthHttpResponseInterceptor = function($q, $location) {
    return {
        response: function (response) {
            if (response.status === 401) {
                console.log("Response 401");
            }
            return response || $q.when(response);
        },
        responseError: function (rejection) {
            if (rejection.status === 401) {
                console.log("Response Error 401", rejection);
                $location.path('/login').search('returnUrl', $location.path());
            }
            return $q.reject(rejection);
        }
    }
}
AuthHttpResponseInterceptor.$inject = ['$q', '$location'];

Now we need to tell our Angular app about, and configure it to use the interceptor. Update AwesomeAngularMVCApp.js like so:

var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ngRoute']);

AwesomeAngularMVCApp.controller('LandingPageController', LandingPageController);
AwesomeAngularMVCApp.controller('LoginController', LoginController);

AwesomeAngularMVCApp.factory('AuthHttpResponseInterceptor', AuthHttpResponseInterceptor);

var configFunction = function ($routeProvider, $httpProvider) {
    $routeProvider.
        when('/routeOne', {
            templateUrl: 'routesDemo/one'
        })
        .when('/routeTwo/:donuts', {
            templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
        })
        .when('/routeThree', {
            templateUrl: 'routesDemo/three'
        })
        .when('/login', {
            templateUrl: '/Account/Login',
            controller: LoginController
        });

    $httpProvider.interceptors.push('AuthHttpResponseInterceptor');
}
configFunction.$inject = ['$routeProvider', '$httpProvider'];

AwesomeAngularMVCApp.config(configFunction);

We now have Angular redirecting properly when authentication fails. We just need something to redirect to.

At the beginning of this tutorial, I asked you to delete the C# AccountController, and now I am about to ask you to create a C# AccountController. This may seem counterproductive, but stick with me. The file you deleted was nearly 500 lines long, and we only need a fraction of that. Also, it's good to know exactly what each line of code does, and not treat Authentication in ASP.NET as a black box.

So go ahead and add a C# Account controller, initially containing the following:

using System.Web.Mvc;

namespace AwesomeAngularMVCApp.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        [AllowAnonymous]
        public ActionResult Login()
        {
            return View();
        }
    }
}

Use Visual Studio to create the Login view, the same way you did before. This view is going to need an Angular controller, so let's add a Javascript file called LoginController.js to Scripts => Controllers, containing the following Javascript:

var LoginController = function($scope, $routeParams) {
    $scope.loginForm = {
        emailAddress: '',
        password: '',
        rememberMe: false,
        returnUrl: $routeParams.returnUrl
    };

    $scope.login = function() {
        //todo
    }
}
LoginController.$inject = ['$scope', '$routeParams'];

The route in AwesomeAngularMVCApp.js needs to be updated so Angular knows to provide the above controller to the login view. We also need to tell our application module that the controller exists:

var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ngRoute']);

AwesomeAngularMVCApp.controller('LandingPageController', LandingPageController);
AwesomeAngularMVCApp.controller('LoginController', LoginController);

var configFunction = function($routeProvider, $routeParams) {
    $routeProvider.
        when('/routeOne', {
            templateUrl: 'routesDemo/one'
        })
        .when('/routeTwo/:donuts', {
            templateUrl: function(params) { return '/routesDemo/two?donuts=' + params.donuts; }
        })
        .when('/routeThree', {
            templateUrl: 'routesDemo/three'
        })
        .when('/login?returnUrl', {
            templateUrl: 'Account/Login',
            controller: LoginController
        });
}
configFunction.$inject = ['$routeProvider'];

AwesomeAngularMVCApp.config(configFunction);

Now lets add the login form itself to the view at Views => Account => Login.cshtml:

<form ng-submit="login()">
    <label for="emailAddress">Email Address:</label>
    <input type="text" ng-model="loginForm.emailAddress" id="emailAddress" required />
    
    <label for="password">Password:</label>
    <input type="password" id="password" ng-model="loginForm.password" required />
    
    <label for="rememberMe">Remember Me:</label>
    <input type="checkbox" id="rememberMe" ng-model="loginForm.rememberMe" required />
    
    <button type="submit">Login</button>
</form>

Now whenever we try to access 'Route three', two things will happen:

We get a 401 unauthorized response, which is logged in the browser console by angular Our inteceptor kicks into action, and redirects us to the Login view we created

We're getting close! We also need a register form for new users, lets create this now.

Add the following Action Method to AccountController.cs.

[AllowAnonymous]
public ActionResult Register()
{
    return View();
}

We need an Angular controller for our Register view. Add a Javascript file called RegisterController.js to Scripts => Controllers containing the following code:

var RegisterController = function($scope) {
    $scope.registerForm = {
        emailAddress: '',
        password: '',
        confirmPassword: ''
    };

    $scope.register = function() {
        //todo
    }
}
RegisterController.$inject = ['$scope'];

Use Visual Studio to create the view, containing the following HTML (hopefully you will start to recognise a pattern):

<form ng-submit="register()">
    <label for="emailAddress">Email Address:</label>
    <input id="emailAddress" type="text" ng-model="registerForm.emailAddress" required />
    
    <label for="password">Password:</label>
    <input id="password" type="password" ng-model="registerForm.password" required />
    
    <label for="confirmPassword">Confirm Password:</label>
    <input id="confirmPassword" type="password" ng-model="registerForm.confirmPassword" required />
    
    <button type="submit">Register</button>
</form>

Now just update AwesomeAngularMVCApp.js with information about this new angular controller and route:

var AwesomeAngularMVCApp = angular.module('AwesomeAngularMVCApp', ['ngRoute']);

AwesomeAngularMVCApp.controller('LandingPageController', LandingPageController);
AwesomeAngularMVCApp.controller('LoginController', LoginController);
AwesomeAngularMVCApp.controller('RegisterController', RegisterController);

AwesomeAngularMVCApp.factory('AuthHttpResponseInterceptor', AuthHttpResponseInterceptor);

var configFunction = function ($routeProvider, $httpProvider) {
    $routeProvider.
        when('/routeOne', {
            templateUrl: 'routesDemo/one'
        })
        .when('/routeTwo/:donuts', {
            templateUrl: function (params) { return '/routesDemo/two?donuts=' + params.donuts; }
        })
        .when('/routeThree', {
            templateUrl: 'routesDemo/three'
        })
        .when('/login', {
            templateUrl: '/Account/Login',
            controller: LoginController
        })
        .when('/register', {
            templateUrl: '/Account/Register',
            controller: RegisterController
        });

    $httpProvider.interceptors.push('AuthHttpResponseInterceptor');
}
configFunction.$inject = ['$routeProvider', '$httpProvider'];

AwesomeAngularMVCApp.config(configFunction);

Now would be a good time to add login and register links to out landing page also:

<!DOCTYPE html>
<html ng-app="AwesomeAngularMVCApp" ng-controller="LandingPageController">
<head>
    <title ng-bind="models.helloAngular"></title>
</head>
<body>
    <h1>{{models.helloAngular}}</h1>

    <ul>
        <li><a href="/#/routeOne">Route One</a></li>
        <li><a href="/#/routeTwo/6">Route Two</a></li>
        <li><a href="/#/routeThree">Route Three</a></li>
    </ul>
    
    <ul>
        <li><a href="/#/login">Login</a></li>
        <li><a href="/#/register">Register</a></li>
    </ul>

    <div ng-view></div>

    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-route.min.js"></script>
    @Scripts.Render("~/bundles/AwesomeAngularMVCApp")
    </body>
</html>

Our C# AccountController needs a little work. It is missing a couple of dependencies so we are going to update it to include an ApplicationUserManager and an ApplicationSignInManager, which are provided through constructor injection, and created on the fly as a fallback. We also need to add methods for logging in and registration:

using Microsoft.AspNet.Identity.Owin;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using AwesomeAngularMVCApp.Models;

namespace AwesomeAngularMVCApp.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        private ApplicationUserManager _userManager;
        private ApplicationSignInManager _signInManager;

        public AccountController() { }

        public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
        {
            UserManager = userManager;
            SignInManager = signInManager;
        }

        public ApplicationUserManager UserManager
        {
            get { return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>(); }
            private set { _userManager = value; }
        }

        public ApplicationSignInManager SignInManager
        {
            get { return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>(); }
            private set { _signInManager = value; }
        }

        [AllowAnonymous]
        public ActionResult Login()
        {
            return View();
        }

        [AllowAnonymous]
        public ActionResult Register()
        {
            return View();
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<bool> Login(LoginViewModel model)
        {
            var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
            switch (result)
            {
                case SignInStatus.Success:
                    return true;
                default:
                    ModelState.AddModelError("", "Invalid login attempt.");
                    return false;
            }
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<bool> Register(RegisterViewModel model)
        {
            var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
            var result = await UserManager.CreateAsync(user, model.Password);
            if (!result.Succeeded) return false;
            await SignInManager.SignInAsync(user, false, false);
            return true;
        }
    }
}

We are very close. The next step is to populate the login and register functions in the respective AngularJS controllers. We don't want to put this logic into the controllers themselves however, a much better approach is to create an Angular factory for each, adhering to the Single Responsibility principle, as well as the principle of least knowledge.

Add a Login factory called LoginFactory.js to Scripts => Factories. Add the following Javascript to the factory:

var LoginFactory = function ($http, $q) {
    return function (emailAddress, password, rememberMe) {

        var deferredObject = $q.defer();

        $http.post(
            '/Account/Login', {
                Email: emailAddress,
                Password: password,
                RememberMe: rememberMe
            }
        ).
        success(function (data) {
            if (data == "True") {
                deferredObject.resolve({ success: true });
            } else {
                deferredObject.resolve({ success: false });
            }
        }).
        error(function () {
            deferredObject.resolve({ success: false });
        });

        return deferredObject.promise;
    }
}

LoginFactory.$inject = ['$http', '$q'];

Now let's tell our Angular application about this factory:

AwesomeAngularMVCApp.factory('LoginFactory', LoginFactory);

Inject this factory into our LoginController, and call it's function when the controllers login function is called:

var LoginController = function ($scope, $routeParams, $location, LoginFactory) {
    $scope.loginForm = {
        emailAddress: '',
        password: '',
        rememberMe: false,
        returnUrl: $routeParams.returnUrl,
        loginFailure: false
    };

    $scope.login = function () {
        var result = LoginFactory($scope.loginForm.emailAddress, $scope.loginForm.password, $scope.loginForm.rememberMe);
        result.then(function(result) {
            if (result.success) {
                if ($scope.loginForm.returnUrl !== undefined) {
                    $location.path('/routeOne');
                } else {
                    $location.path($scope.loginForm.returnUrl);
                }
            } else {
                $scope.loginForm.loginFailure = true;
            }
        });
    }
}

LoginController.$inject = ['$scope', '$routeParams', '$location', 'LoginFactory'];

We also need a minor update to our Login view:

<form ng-submit="login()">
    <label for="emailAddress">Email Address:</label>
    <input type="email" ng-model="loginForm.emailAddress" id="emailAddress"/>
    
    <label for="password">Password:</label>
    <input type="password" id="password" ng-model="loginForm.password"/>
    
    <label for="rememberMe">Remember Me:</label>
    <input type="checkbox" id="rememberMe" ng-model="loginForm.rememberMe" />
    
    <button type="submit">Login</button>
</form>

<div ng-if="loginForm.loginFailure">
    D'oh!
</div>

If all is right with the world, attempting to login should yield the following result:

D'oh

That's because we don't have any users. Let's create our Registration factory. Add RegistrationFactory.js to Scripts => Factories and add the following Javascript:

var RegistrationFactory = function ($http, $q) {
    return function (emailAddress, password, confirmPassword) {

        var deferredObject = $q.defer();

        $http.post(
            '/Account/Register', {
                Email: emailAddress,
                Password: password,
                ConfirmPassword: confirmPassword
            }
        ).
        success(function (data) {
            if (data == "True") {
                deferredObject.resolve({ success: true });
            } else {
                deferredObject.resolve({ success: false });
            }
        }).
        error(function () {
            deferredObject.resolve({ success: false });
        });

        return deferredObject.promise;
    }
}

RegistrationFactory.$inject = ['$http', '$q'];

Now tell Angular about the new factory:

AwesomeAngularMVCApp.factory('RegistrationFactory', RegistrationFactory); Then inject it into RegisterController, and call it's function:

var RegisterController = function ($scope, $location, RegistrationFactory) {
    $scope.registerForm = {
        emailAddress: '',
        password: '',
        confirmPassword: '',
        registrationFailure: false
    };

    $scope.register = function () {
        var result = RegistrationFactory($scope.registerForm.emailAddress, $scope.registerForm.password, $scope.registerForm.confirmPassword);
        result.then(function (result) {
            if (result.success) {
                $location.path('/routeOne');
            } else {
                $scope.registerForm.registrationFailure = true;
            }
        });
    }
}

RegisterController.$inject = ['$scope', '$location', 'RegistrationFactory'];

Right now, we should be able to run our application, register as a user and view 'Route Three'.