AngularJS Services for Views: Configuring Routes

The routes I have defined so far in this chapter have defined only the templateUrl configuration property, which specifies the URL of the view file that the route will display. This is only one of the configuration options available. I have listed the full set in Table 22-5, and I describe the two most important, controller and resolve, in the sections that follow.

1. Using Controllers with Routes

If you have lots of views in an application, having them share a single controller (as I have been doing so far in this chapter) becomes unwieldy to manage and test. The controller configuration option allows you to specify a controller that has been registered through the Module.controller method for the view. The effect is to separate out the controller logic that is unique to each view, as shown in Listing 22-11.

Listing 22-11. Using a Controller with a View in the products.js File

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

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

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

$locationProvider.html5Mode(true);

$routeProvider.when(“/edit/:id”, {

templateUrl: “/editorView.html”,

controller: “editCtrl”

});

$routeProvider.when(“/create”, {

templateUrl: “/editorView.html”,

controller: “editCtrl”

});

$routeProvider.otherwise({

templateUrl: “/tableView.html”

});

})

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

$scope.productsResource = $resource(baseUrl + { id: “i®id” },

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

$scope.listProducts = function () {

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

}

$scope.createProduct = function (product) {

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

$scope.products.push(newProduct);

$location.path(“/list”);

});

}

$scope.deleteProduct = function (product) {

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

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

});

$location.path(“/list”);

}

$scope.listProducts();

})

.controller(“editCtrl”, function ($scope, $routeParams, $location) {

$scope.currentProduct = null;

if ($location.path().indexOf(“/edit/”) == 0) {

var id = $routeParams[“id”];

for (var i = 0; i < $scope.products.length; i++) {

if ($scope.products[i].id == id) {

$scope.currentProduct = $scope.products[i]; break;

}

}

}

$scope.cancelEdit = function () {

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

$scope.cuxxentProduct.$get();

}

$scope.currentProduct = {};

$location.path(“/list”);

}

$scope.updateProduct = function (product) {

product.$save();

$location.path(“/list”);

}

$scope.saveEdit = function (product) {

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

$scope.updateProduct(product);

} else {

$scope.createProduct(product);

}

$scope.currentProduct = {};

}

});

I have defined a new controller called editCtrl and moved the code from the defaultCtrl controller that is unique to supporting the editorView.html view. I then associate this controller with the routes that display the editorView.html file using the controller configuration property.

A new instance of the editCtrl controller will be created each time that the editorView.html view is displayed, which means I don’t need to use the $route service events to know when the view has changed. I can just rely on the fact that my controller function is being executed.

One of the nice aspects of using controllers in this way is that the standard inheritance rules that I described in Chapter 13 apply, such that the editCtrl is nested within the defaultCtrl and can access the data and behaviors defined in its scope. This means I can define the common data and functionality in the top-level controller and just define the view-specific features in the nested controllers.

2. Adding Dependencies to Routes

The resolve configuration property allows you to specify dependencies that will be injected into the controller specified with the controller property. These dependencies can be services, but the resolve property is more useful for performing work required to initialize the view. This is because you can return promise objects as dependencies, and the route won’t instantiate the controller until they are resolved. In Listing 22-12, you can see how I have added a new controller to the example and used the resolve property to load the data from the server.

Listing 22-12. Using the resolve Configuration Property in the products.js File

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

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

.factory(“productsResource”, function ($resource, baseUrl) {

return $resource(baseUrl + “:id”, { id: “@id” },

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

})

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

$locationProvider.html5Mode(true);

$routeProvider.when(“/edit/:id”, {

templateUrl: “/editorView.html”,

controller: “editCtrl”

});

$routeProvider.when(“/create”, {

templateUrl: “/editorView.html”,

controller: “editCtrl”

});

$routeProvider.otherwise({

templateUrl: “/tableView.html”,

controller: “tableCtrl”, resolve: {

data: function (productsResource) {

return productsResource.query();

}

}

});

})

.controller(“defaultCtrl”, function ($scope, $location, productsResource) {

$scope.data = {};

$scope.createProduct = function (product) {

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

$scope.data.products.push(newProduct);

$location.path(“/list”);

});

}

$scope.deleteProduct = function (product) {

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

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

});

$location.path(“/list”);

}

})

.controller(“tableCtrl”, function ($scope, $location, $route, data) {

$scope.data.products = data;

$scope.refreshProducts = function () {

$route.reload();

}

})

.controller(“editCtrl”, function ($scope, $routeParams, $location) {

$scope.currentProduct = null;

if ($location.path().indexOf(“/edit/”) == 0) {

var id = $routeParams[“id”];

for (var i = 0; i < $scope.data.products.length; i++) {

if ($scope.data.products[i].id == id) {

$scope.currentProduct = $scope.data.products[i];

break;

}

}

$scope.cancelEdit = function () {

$location.path(“/list”);

}

$scope.updateProduct = function (product) {

product.$save(); $location.path(“/list”);

}

$scope.saveEdit = function (product) {

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

$scope.updateProduct(product);

} else {

$scope.createProduct(product);

}

$scope.currentProduct = {};

}

});

There are a lot of changes in the listing, so I’ll walk you through them in turn. The most important is the change of the definition of the /list route so that it uses the controller and resolve properties, like this:

$routeProvider.otherwise({

templateUrl: “/tableView.html”,

controller: “tableCtrl”, resolve: {

data: function (productsResource) {

return productsResource.query();

}

}

});

I have specified that the route should instantiate a controller called tableCtrl, and I have used the resolve property to create a dependency called data. The data property is set to a function that will be evaluated before the tableCtrl controller is created, and the result will be passed as an argument called data.

For this example, I use the $resource access object to obtain the data from the server, which means that the controller won’t be instantiated until it is loaded and that, as a consequence, the tableView.html view won’t be displayed until then either.

To be able to access the access object from the route dependency, I have to create a new service, as follows:

.factory(“productsResource”, function ($resource, baseUrl) {

return $resource(baseUrl + “:id”, { id: “@id” },

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

})

This is the same code that I used to create the productResource object in the controller in previous listings, just moved to a service through the factory method (described in Chapter 18) so that it is accessible more widely in the application.

The tableCtrl controller is rather simple, as follows:

.controller(“tableCtrl”, function ($scope, $location, $route, data) {

$scope.data.products = data;

$scope.refreshProducts = function () {

$route.reload();

}

})

I receive the product information from the server via the data argument and simply assign it to the $scope.data.products property. As I explained in the previous sections, the controller/scope inheritance rules that I described in Chapter 13 apply when using controllers with routes, so I had to add an object to which the data property belongs to ensure that the product data is available to all of the controllers in the applications and not just the scope belonging to the tabelCtrl controller.

The effect of adding the dependency in the route is that I no longer need the listProducts behavior, so I removed it from the defaultCtrl controller. That stranded the Refresh button in the tableView.html view without a way to force reload the data, so I defined a new behavior called refreshProducts, which uses the $route.reload method I described in Table 22-3. The final JavaScript change was to simplify the cancelEdit behavior, which no longer needs to reload a single product object from the server when editing is cancelled because all of the data will be refreshed when the /list route is activated:

$scope.cancelEdit = function () {

$scope.currentProduct = {};

$location.path(“/list”);

}

To reflect the changes in the controller, I had to update the tableView.html file, as shown in Listing 22-13.

Listing 22-13. Updating the tableView.html File to Reflect Controller Changes

<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 data.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>

<a href=”/edit/{{item.id}}” class=”btn btn-xs btn-primary”>Edit</a>

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

method-name=”$save” />

</td>

</tr>

</tbody>

</table>

<div>

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

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

</div>

</div>

There are two simple changes. The first is to update the ng-repeat directive to reflect the new data structure I put in place to deal with the scope hierarchy. The second is to update the Refresh button so that it calls the refreshProducts behavior instead of the defunct listProducts. The overall effect is that the data is obtained from the server automatically when the /list view is activated, which simplifies the overall code in the application.

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 *