ASP.NET 5 and AngularJS Part 3, Adding Client Routing

This is the third part in a multiple part blog series on building ASP.NET 5 (ASP.NET vNext) apps with AngularJS. In this series of blog posts, I show how you can create a simple Movie app using ASP.NET 5, MVC 6, and AngularJS.

You can download the code discussed in this blog post from GitHub:

https://github.com/StephenWalther/MovieAngularJSApp

In this blog post, I explain how to divide your single page app (SPA) into multiple virtual pages. I’ll use AngularJS routing to create distinct list, add, edit, and delete views.

Define the Client-Side Routes

The first step is to define the client-side routes. You define these routes in the same JavaScript file – app.js – in which you created your AngularJS app.

(function () {
    'use strict'; 

    config.$inject = ['$routeProvider', '$locationProvider']; 

    angular.module('moviesApp', [
        'ngRoute', 'moviesServices'
    ]).config(config);

    function config($routeProvider, $locationProvider) {
        $routeProvider
            .when('/', {
              templateUrl: '/Views/list.html',
              controller: 'MoviesListController'
            })
            .when('/movies/add', {
                templateUrl: '/Views/add.html',
                controller: 'MoviesAddController'
            })
            .when('/movies/edit/:id', {
                templateUrl: '/Views/edit.html',
                controller: 'MoviesEditController'
            })
            .when('/movies/delete/:id', {
                templateUrl: '/Views/delete.html',
                controller: 'MoviesDeleteController'
            });

        $locationProvider.html5Mode(true); 
    }

})();

In the code above, I added 4 routes. For each route I defined a templateUrl that provides the location of the HTML for the route. I also associated each route with an AngularJS controller.

You should also notice that I have added ngRoute as a dependency for the moviesApp. I won’t get any of the routing functionality without adding a dependency on the ngRoute module:

angular.module('moviesApp', [
        'ngRoute', 'moviesServices'
    ]).config(config);

Finally, notice that I enabled html5Mode in the last line of code. Enabling html5Mode allows you to use natural URLs that look like:

https://movies.com/movies/add/

Instead of unnatural hashbang URLs that look like:

http://movies.com/#!/movies/add/

Using html5mode is compatible with any browser that supports the HTML5 History API (IE10+ and every other browser).

Rewrite Requests on the Server

If you navigate to /movies/add and hit the Reload button, then you’ll get a 404 response from the server. You’ll get a 404 error because /movies/add is a client-side route and not a server-side route.

To fix this issue, you need to configure IIS to redirect all requests back to home. Add the following web.config file to your wwwroot folder:

<!-- from http://stackoverflow.com/questions/25916851/wrapping-staticfilemiddleware-to-redirect-404-errors -->

<configuration>
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true" />
  <rewrite>
    <rules>
      <!--Redirect selected traffic to index -->
      <rule name="Index Rule" stopProcessing="true">
        <match url=".*" />
        <conditions logicalGrouping="MatchAll">
          <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
          <add input="{REQUEST_URI}" matchType="Pattern" pattern="^/api/" negate="true" />
        </conditions>
        <action type="Rewrite" url="/index.html" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>
</configuration>  

Notice that I am also enabling runAllManagedModulesForAllRequests (RAMMFAR). I need to enable RAMMFAR to handle PUT and DELETE requests.

Creating the Client Controllers

We need a separate client controller to correspond to each of our routes. Here’s how the client controllers are defined in the moviesController.js file:

(function () {
    'use strict';

    angular
        .module('moviesApp')
        .controller('MoviesListController', MoviesListController)
        .controller('MoviesAddController', MoviesAddController)
        .controller('MoviesEditController', MoviesEditController)
        .controller('MoviesDeleteController', MoviesDeleteController);

    /* Movies List Controller  */
    MoviesListController.$inject = ['$scope', 'Movie'];

    function MoviesListController($scope, Movie) {
        $scope.movies = Movie.query();
    }

    /* Movies Create Controller */
    MoviesAddController.$inject = ['$scope', '$location', 'Movie'];

    function MoviesAddController($scope, $location, Movie) {
        $scope.movie = new Movie();
        $scope.add = function () {
            $scope.movie.$save(function () {
                $location.path('/');
            });
        };
    }

    /* Movies Edit Controller */ 
    MoviesEditController.$inject = ['$scope', '$routeParams', '$location', 'Movie'];

    function MoviesEditController($scope, $routeParams, $location, Movie) {
        $scope.movie = Movie.get({ id: $routeParams.id });
        $scope.edit = function () {
            $scope.movie.$save(function () {
                $location.path('/');
            });
        };
    }

    /* Movies Delete Controller  */
    MoviesDeleteController.$inject = ['$scope', '$routeParams', '$location', 'Movie'];

    function MoviesDeleteController($scope, $routeParams, $location, Movie) {
        $scope.movie = Movie.get({ id: $routeParams.id });
        $scope.remove = function () {
            $scope.movie.$remove({id:$scope.movie.Id}, function () {
                $location.path('/');
            });
        };
    }

    
})();

Notice that I don’t create a single controller with multiple actions (as I would in the case of server-side MVC). Instead, I create a separate controller that corresponds to the list, add, edit, and delete actions.

Each controller interacts with the ASP.NET Web API with the help of a Movie resource. For example, the MoviesListController retrieves the list of movies from the server by calling Movie.query(). And, the MoviesAddController posts a new movie to the server by invoking movie.$save().

The Movie resource is defined in a module named moviesServices.js:

(function () {
    'use strict';

    angular
        .module('moviesServices', ['ngResource'])
        .factory('Movie', Movie);

    Movie.$inject = ['$resource'];

    function Movie($resource) {
        return $resource('/api/movies/:id');
    }
})();

Creating the Main Layout

The index.html page in the Movies app now works like a Master Page/Layout Page. It contains the content that will be shared with all pages. Here’s the updated content for the index.html page:

<!DOCTYPE html>
<html ng-app="moviesApp">
<head>
    <base href="/">
    <meta charset="utf-8" />
    <title>Movies</title>

    <!-- jQuery -->
    <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>

    <!-- AngularJS-->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-resource.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-route.js"></script>
    <script src="app.js"></script>
</head>
<body ng-cloak>
    <div class="container-fluid">
        <ng-view></ng-view>
    </div>
</body>
</html>

There are three main things that you should notice about my updated index.html page. First, notice that I have added Twitter Bootstrap to my app by grabbing the Bootstrap styles and script from the maxcdn bootstrap CDN.

Second, notice that I have pulled the AngularJS ngRoute module in from the Google CDN with the following tag:

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-route.js"></script>

Finally, notice that I am using the AngularJS directive (custom element) to mark where I want the content pages to be added to the index.html page. For example, when I navigate to /movies/add then the add.html page will be loaded into the location marked by the directive.

Creating the Virtual Pages

The Movies app has 4 virtual pages for doing CRUD operations:

  • list.html – Lists the movies in a grid.
  • add.html – Enables you to add new movies.
  • edit.html – Enables you to edit existing movies.
  • delete.html – Enables you to delete existing movies.

Here’s what the list.html page looks like:

<div>

    <h1>List Movies</h1>
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th></th>
                <th>Title</th>
                <th>Director</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="movie in movies">
                <td>
                    <a href="/movies/edit/{{movie.Id}}" class="btn btn-default btn-xs">edit</a>
                    <a href="/movies/delete/{{movie.Id}}" class="btn btn-danger btn-xs">delete</a>
                </td>
                <td>{{movie.Title}}</td>
                <td>{{movie.Director}}</td>
            </tr>
        </tbody>
    </table>

    <p>
        <a href="/movies/add" class="btn btn-primary">Add New Movie</a>
    </p>
</div>

The list.html page uses the ng-repeat directive to display the list of movies in an HTML table. I styled the table with Twitter Bootstrap.

ListMovies

The add.html page contains an HTML form created with the help of Boostrap:

<h1>Add Movie</h1>
<div class="row">
    <div class="col-md-5">

        <form ng-submit="add()">
            <div class="form-group">
                <label for="Title">Movie Title</label>
                <input name="Title" type="text" class="form-control" placeholder="Enter title" ng-model="movie.Title">
            </div>
            <div class="form-group">
                <label for="Director">Director</label>
                <input name="Director" type="text" class="form-control" placeholder="Enter director" ng-model="movie.Director">
            </div>
            <a href="/" class="btn btn-default">Cancel</a>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>
    </div>
</div>

AddMovie

Notice that the <input> elements include ng-model attributes. These attributes create a two-way data-binding between the form fields and the model. When you click the Submit button, the ng-submit attribute on the <form> element invokes the add() method defined in the controller.

The edit.html page looks very similar to the add.html page:

<h1>Edit Movie</h1>

<div class="row">
    <div class="col-md-5">

        <form ng-submit="edit()">
            <div class="form-group">
                <label for="Title">Movie Title</label>
                <input name="Title" type="text" class="form-control" placeholder="Enter title" ng-model="movie.Title">
            </div>
            <div class="form-group">
                <label for="Director">Director</label>
                <input name="Director" type="text" class="form-control" placeholder="Enter director" ng-model="movie.Director">
            </div>
            <a href="/" class="btn btn-default">Cancel</a>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>


    </div>
</div>

The ng-model attributes applied to the <input> elements cause the form to be prefilled with the property values of the movie being edited. So we get the title “Star Wars” and director “Lucas” automatically.

EditMovie

When you submit the edit form, controller the edit() method is invoked.

Finally, the delete.html page is used to confirm that you really want to delete a movie from the database.

DeleteMovie

Here’s the contents of the delete.html page:

<div class="alert alert-warning">
    Are you sure that you want to permanently delete the movie
    &quot;{{movie.Title}}&quot;?
</div>

<a href="/" class="btn btn-default">Cancel</a>
<button class="btn btn-danger" ng-click="remove()">OK</button>

When you click the Delete button, the remove() method is invoked.

Summary

In this blog post, I focused on client-side routing using the AngularJS ngRoute module. You learned how to use client-side routing with an ASP.NET 5 app.

Currently, you can’t actually create new movies or edit existing movies. In the next blog post, I’ll explain how you can use Entity Framework 7 to save changes to a database.

Discussion

  1. ard says:

    Thanks for this series. If you can put a little bit more info on stuff like $inject, $resource and similar angular items with their purpose – it would help alot.

    I think for the edit and delete views to bind to model, we need the web api controller to have a GET by id method.

  2. @ard — Thanks for reading my blog post! Take a look at the next 2 blog posts in this series. I made several updates to both the client-side and server-side controllers.

  3. ard says:

    Thanks. Got the code for part 3 from GitHub and now it looks good. I didnt go to next blog post because my screens were not looking like your screenshots in this part so I thought I am doing something wrong. Once I added the Get by id method, they started working fine.

  4. Mohammad says:

    Thanks for yet another nice article.
    I believe that in order to demonstrate vNEXT, MVC 6 and EF 7 optimal it would have been better if you had avoided to include AngularJS, since it is only causing distraction and complicating things unnecessarily. jQuery would have been a better choice since its is more easy to understand and it does not ruin/interrupt the natural flow of MVC.