AngularJS Services for Views: Using URL Routing

AngularJS supports a feature called URL routing, which uses the value returned by the $location.path method to load and display view files without the need for nasty literal values embedded throughout the markup and code in an application. In the sections that follow, I’ll show you how to install and use the $route service, which provides the URL routing functionality.

1. Installing the ngRoute Module

The $route service is defined within an optional module called ngRoute that must be downloaded into the angularjs folder. Go to http://angularjs.org, click Download, select the version you require (version 1.2.5 is the latest version as I write this), and click the Extras link in the bottom-left corner of the window, as shown in Figure 22-1.

Download the angular-route.js file into the angularjs folder. In Listing 22-3, you can see how I have added a script element for the new file to the products.html file.

Listing 22-3. Adding a Reference to the products.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Products</title>

<script src=”angular.js”></script>

<script src=”angular-resource.js”></script>

<script src=”angular-route.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”products.js”></script>

<script src=”increment.js”></script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-primary”>

<h3 class=”panel-heading”>Products</h3>

<ng-include src=”‘tableView.html'” ng-show=”displayMode == ‘list'”></ng-include>

<ng-include src=”‘editorView.html'” ng-show=”displayMode == ‘edit'”></ng-include>

</div>

</body>

</html>

2. Defining the URL Routes

At the heart of the functionality provided by the $route service is a set of mappings between URLs and view file names, known as URL routes or just routes. When the value returned by the $location.path method matches one of the mappings, the corresponding view file will be loaded and displayed. The mappings are defined using the provider for the $route service, $routeProvider. Listing 22-4 shows how I have defined routes for the example application.

Listing 22-4. Defining Routes in the product.js File

angular.module(“exampleApp”, [“increment”, “ngResource”, “ngRoute”])

.constant(“baseUrl”, “http://localhost:5500/products/”)

.config(function ($routeProvider, $locationProvider) {

$locationProvider.html5Mode(true);

$routeProvider.when(“/list”, {

templateUrl: “/tableView.html”

});

$routeProvider.when(“/edit”, {

templateUrl: “/editorView.html”

});

$routeProvider.when(“/create”, {

templateUrl: “/editorView.html” });

$routeProvider.otherwise({

templateUrl: “/tableView.html”

});

})

.controller(“defaultCtrl”, function ($scope, $http, $resource, baseUrl) {

// …controller statements omitted for brevity…

});

I have added a dependency on the ngRoute module and added a config function to define the routes. My config function declares dependencies on providers for the $route and $location services, the latter of which I use to enable HTML5 URLs.

Tip I am going to use HTML5 URLs in this chapter because they are cleaner and simpler and I know that the browser I will be using supports the HTML5 History API. See Chapter 19 for details of the $location service support for HTML5, how to detect that the browser provides the required features, and the potential for problems.

Routes are defined using the $routeProvider.when method. The first argument is the URL that the route will apply to, and the second argument is the route configuration object. The routes I have defined are the simplest possible because the URLs are static and I have provided the minimum configuration information, but later in the chapter I’ll show you more complex examples. I’ll describe all of the configuration options later in the chapter, but for now it is enough to know that the templateUrl configuration option specifies the view file that should be used when the path of the current browser URL matches the first argument passed to the when method.

Tip Always specify the value of the templateUrl with a leading / character. If you do not, the URL will be evaluated relative to the value returned by the $location.path method, and changing this value is one of the key activities required when using routing. Without the / character, you will quickly generate a Not Found error as you navigate within the application.

The otherwise method is used to define a route that is used when no other one matches the current URL path. It is good practice to provide such a fallback route, and I have summarized the overall effect of the routes I have defined in Table 22-2.

Tip I didn’t really need to define the route for /list since the route defined with the otherwise method displays the tableView.html view if no other route matches the current path. I like to be explicit when defining routes because they can become quite complex, and anything that makes them easier to read and understand is worth doing.

3. Displaying the Selected View

The ngRoute module includes a directive called ng-view that displays the contents of the view file specified by the route that matches the current URL path returned by the $location service. In Listing 22-5, you can see how I am able to use the ng-view directive to replace the troublesome elements in the products.html file, removing the literal values that I dislike so much.

Listing 22-5. Using the ng-view Directive in the products.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Products</title>

<script src=”angular.js”></script>

<script src=”angular-resource.js”></script>

<script src=”angular-route.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script src=”products.js”></script>

<script src=”increment.js”></script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-primary”>

<h3 class=”panel-heading”>Products</h3>

<div ng-view></div>

</div>

</body>

</html>

When the value returned by the $location/path changes, the $route service evaluates the routes defined through its provider and changes the content of the element to which the ng-view directive has been applied.

4. Wiring Up the Code and Markup

All that remains is to update the code and the markup to change the URL rather than the displayMode variable to change the layout of the application. In JavaScript code, this means I need to use the path method provided by the $location service, as shown in Listing 22-6.

Listing 22-6. Using the $location Service to Change Views in the products.js File

angular.module(“exampleApp”, [“increment”, “ngResource”, “ngRoute”])

.constant(“basellrl”, “http://localhost:5500/products/”)

.config(function ($routeProvider, $locationProvider) {

$locationProvider.html5Mode(true);

$routeProvider.when(“/list”, {

templatelrl: “/tableView.html”

});

$routeProvider.when(“/edit”, {

templatelrl: “/editorView.html”

});

$routeProvider.when(“/create”, {

templatelrl: “/editorView.html”

});

$routeProvider.otherwise({

templatelrl: “/tableView.html”

});

})

.controller(“defaultCtrl”, function ($scope, $http, $resource, $location, baseUrl) {

$scope.currentProduct = null;

$scope.productsResource = $resource(baselrl + “:id”, { id: “@id” },

{ create: { method: “POST” }, save: { method: “PUT” } });

$scope.listProducts = function () {

$scope.products = $scope.productsResource.query();

}

$scope.deleteProduct = function (product) {

product.$delete().then(function () {

$scope.products.splice($scope.products.indexOf(product), 1);

});

$location.path(“/list”);

}

$scope.createProduct = function (product) {

new $scope.productsResource(product).$create().then(function (newProduct) {

$scope.products.push(newProduct);

$location.path(“/list”);

});

}

$scope.updateProduct = function (product) {

product.$save();

$location.path(“/list”);

}

$scope.editProduct = function (product) {

$scope.currentProduct = product;

$location.path(“/edit”);

}

$scope.saveEdit = function (product) {

if (angular.isDefined(product.id)) {

$scope.updateProduct(product);

} else {

$scope.createProduct(product);

}

$scope.currentProduct = {};

}

$scope.cancelEdit = function () {

if ($scope.currentProduct && $scope.currentProduct.$get) {

$scope.currentProduct.$get();

}

$scope.currentProduct = {};

$location.path(“/list”);

}

$scope.listProducts();

});

This isn’t a huge change. I have added a dependency on the $location service and replaced the calls that changed the displayMode value with equivalent calls to the $location.path method. There is a more interesting change, however: I replaced the editOrCreateProduct behavior with one called editProduct, which is slightly simpler. Here is the old behavior:

$scope.editOrCreateProduct = function (product) {

$scope.currentProduct = product ? product : {};

$scope.displayMode = “edit”;

}

And here is its replacement:

$scope.editProduct = function (product) {

$scope.currentProduct = product;

$location.path(“/edit”);

The old behavior was the start point for both the editing and creation process, which were differentiated by the product argument. If the product argument wasn’t null, then I used the object to set the currentProduct variable, which populates the fields in the editorView.html view.

Tip There is one other change highlighted in the listing. I have updated the saveEdit behavior to reset the value of the currentProduct variable. Without this change, the values from an edit operation are displayed to the user if they subsequently create a new product. This is a temporary problem that will be resolved as I expand the support for routing in the application.

The reason I am able to simplify the behavior is that the routing feature allows me to initiate the process of creating a new product object just by changing the URL. In Listing 22-7, you can see the changes I have made to the tableView.html file.

Listing 22-7. Adding Support for Routes to the tableView.html File

<div class=”panel-body”>

<table class=”table table-striped table-bordered”>

<thead>

<tr>

<th>Name</th>

<th>Category</th>

<th class=”text-right”>Price</th>

<th></th>

</tr>

</thead>

<tbody>

<tr ng-repeat=”item in products”>

<td>{{item.name}}</td>

<td>{{item.category}}</td>

<td class=”text-right”>{{item.price | currency}}</td>

<td class=”text-center”>

<button class=”btn btn-xs btn-primary”

ng-click=”deleteProduct(item)”>

Delete

</button>

<button class=”btn btn-xs btn-primary” ng-click=”editProduct(item)”>

Edit

</button>

<increment item=”item” property-name=”price” restful=”true”

method-name=”$save” />

</td>

</tr>

</tbody>

</table>

<div>

<button class=”btn btn-primary” ng-click=”listProducts()”>Refresh</button>

<a href=”create” class=”btn btn-primary”>New</a>

</div>

</div>

I have replaced the button element whose ng-click directive invoked the old behavior and replaced it with an a element whose href attribute specifies the URL that matches the route that displays the editorView.html view. Bootstrap allows me to style button and a elements to look the same, so there is no discernable difference in the layout to the user. However, when the a element is clicked, the URL changes to /create and the editorView.html view is displayed, as shown in Figure 22-2.

To see the effect, load the products.html file into the browser and click the New button. The URL displayed by the browser will change from http://localhost:5000/products.html to http://localhost:5000/create. This is the magic of HTML5 URLs managed through the HTML5 Browser History API, and the contents of the editorView.html view will be displayed. Enter details of a new product and click the Save button (or Cancel if you prefer), and the contents of the tableView.html view are shown again, with a URL of http://localhost:5000/list.

Caution Routing works when the application changes the URL, but it doesn’t work if the user changes it; the browser takes any URL that the user enters as being a literal request for a file and tries to request the corresponding content from the server.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *