SportsStore – A Application of AngularJS: Displaying the Category List

The next step is to display the list of categories so that the user can filter the set of products that are displayed. Implementing this feature requires the generation of the elements with which the user will navigate, handling the navigation to select a product category, and, finally, updating the details pane so that only products in the selected category are displayed.

1. Creating a List of Categories

I want to generate the category elements dynamically from the product data objects, rather than hard-code HTML elements for a fixed set of categories. The dynamic approach is more complex to set up, but it will allow the SportsStore application to automatically reflect changes in the product catalog. This means I have to be able to generate a list of unique category names from an array of product data objects. This is a feature that AngularJS doesn’t include but is easy to implement by creating and applying a custom filter. I created a file called customFilters.js in the filters directory, and you can see the contents of this file in Listing 6-4.

Listing 6-4. The Contents of the customFilters.js File

angular.module(“customFilters”, [])

.filter(“unique”, function () {

return function (data, propertyName) {

if (angular.isArray(data) && angular.isString(propertyName)) {

var results = [];

var keys = {};

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

var val = data[i][propertyName];

if (angular.isUndefined(keys[val])) {

keys[val] = true;

results.push(val);

}

}

return results;

} else {

return data;

}

}

});

Custom filters are created using the filter method defined by Module objects, which are obtained or created through the angular.module method. I have chosen to create a new module, called customFilters, to contain my filter, mainly so I can show you how to define and combine multiple modules within an application.

Tip There are no hard-and-fast rules about when you should add a component to an existing module or create a new one. I tend to create modules when I am defining functionality that I expect to reuse in a different application later. Custom filters tend to be reusable because data formatting is something that almost all AngularJS applications require and most developers end up with a utility belt of common formats they require.

The arguments to the filter method are the name of the filter, which is unique in this case, and a factory function that returns a filter function that does the actual work. AngularJS calls the factory function when it needs to create an instance of the filter, and the filter function is invoked to perform the filtering.

All filter functions are passed the data they are being asked to format, but my filter defines an additional argument, called propertyName, which I use to specify the object property that will be used to generate a list of unique values. You’ll see how to specify the value for the propertyName argument when I apply the filter. The implementation of the filter function is simple: I enumerate the contents of the data array and build up a list of the unique values of the property whose name is provided through the propertyName argument.

Tip I could have hard-coded the filter function to look for the category property, but that limits the potential for reusing the unique filter elsewhere in the application or even in another AngularJS application. By taking the name of the property as an argument, I have created a filter that can be used to generate a list of the unique values of any property in a collection of data objects.

A filter function is responsible for returning the filtered data, even if it is unable to process the data it receives.

To that end, I check to see that the data I am working with is an array and that the propertyName is a string—checks that I perform using the angular.isArray and angular.isString methods. Later in the code, I check to see whether a property has been defined using the angular.isUndefined method. AngularJS provides a range of useful utility methods, including ones that allow you to check the type of objects and properties. I describe the complete set of these methods in Chapter 5. If my filter has received an array and a property name, then I generate and return an array of the unique property values. Otherwise, I return the data I have received unmodified.

Tip Changes that a filter makes to the data affect only the content displayed to the user and do not modify the original data in the scope.

1.1. Generating the Category Navigation Links

The next step is to generate the links that the user will click to navigate between product categories. This requires the use of the unique filter that I created in the previous section, along with some useful built-in AngularJS features, as shown in Listing 6-5.

Listing 6-5. Generating the Navigation Links in the app.html File

<!DOCTYPE html>

<html ng-app=”sportsStore”>

<head>

<title>SportsStore</title>

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

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

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

<script>

angular.module(“sportsStore”, [“customFilters”])}

</script>

<script src=”controllers/sportsStore.js”></script>

<script src=”filters/customFilters.js”></script>

</head>

<body ng-controller=”sportsStoreCtrl”>

<div class=”navbar navbar-inverse”>

<a class=”navbar-brand” href=”#”>SPORTS STORE</a>

</div>

<div class=”panel panel-default row”>

<div class=”col-xs-3″>

<a ng-click=”selectCategory()”

class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”>

{{item}}

</a>

</div>

<div class=”col-xs-8″>

<div class=”well” ng-repeat=”item in data.products”>

<h3>

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

<span class=”pull-right label label-primary”>

{{item.price | currency}}

</span>

</h3>

<span class=”lead”>{{item.description}}</span>

</div>

</div>

</div>

</body>

</html>

The first change that I made in this listing was to update the definition of the sportsStore module to declare a dependency on the customFilters module that I created in Listing 6-4 and that contains the unique filter:

angular.module(“sportsStore”, [“customFilters“]);

This is known as declaring a dependency. In this case, I am declaring that the sportsStore module depends on the functionality in the customFilters module. This causes AngularJS to locate the customFilters module and make it available so that I can refer to the components it contains, such as filters and controllers—a process known as resolving the dependency.

Tip The process of declaring and managing dependencies between modules and other kinds of components—known as dependency injection—is central to AngularJS. I explain the process in Chapter 9.

I also have to add a script element that loads the contents of the file that contains the customFilters module, as follows:

<script>

angular.module(“sportsStore”, [“customFilters”]);

</script>

<script src=”controllers/sportsStore.js”></script>

<script src=”filters/customFilters.js”></script>

Notice that I am able to define the script element for the customFilters.js file after the one that creates the sportsStore module and declares a dependency on the customFilters module. This is because AngularJS loads all of the modules before using them to resolve dependencies. The effect can be confusing: The order of the script elements is important when you are extending a module (because the module must already have been defined) but not when defining a new module or declaring a dependency on one. The final set of changes in Listing 6-5 generates the category selection elements. There is quite a lot going on in these elements, and it will be easier to understand if you know what the result looks like—the addition of the category buttons—shown in Figure 6-8.

1.2. Generating the Navigation Elements

The most interesting part of the markup is the use of the ng-repeat element to generate an a element for each product category, as follows:

<a ng-click=”selectCategory()” class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”>

{{item}}

</a>

The first part of the ng-repeat attribute value is the same as the one I used when generating the product details, item in data.products, and tells the ng-repeat directive that it should enumerate the objects in the data.products array, assign the current object to a variable called item, and duplicate the a element to which the directive has been applied.

The second part of the attribute value tells AngularJS to pass the data.products array to a built-in filter called orderBy, which is used to sort arrays. The orderBy filter takes an argument that specifies which property the objects will be sorted by, which I specify by placing a colon (the : character) after the filter name and then the argument value. In this example, I have specified that the category property be used. (I describe the orderBy filter fully in Chapter 14.)

Tip Notice that I have specified the name of the property between single quotes (the ‘ character). By default, AngularJS assumes that names in expression refer to variables defined on the scope. To specify a static value, I have to use a string literal, which requires the single quote characters in JavaScript. (I could have used double quotes, but I already used them to demark the start and end of the ng-repeat directive attribute value.)

The use of the orderBy filter puts the product objects in order, sorted by the value of their category property. But one of the nice features of filters is that you can chain several together by using the bar symbol (the | character) and the name of another filter. In this case, I have used the unique filter that I developed earlier in the chapter. AngularJS applies filters in the order in which they are applied, which means that the objects are sorted by the category property and only then passed to the unique filter, which generates the set of unique category values. You can see how I have specified the property the unique filter will operate on:

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

The effect is that the data.products array is passed to the orderBy filter, which sorts the objects based on the value of the category property. The sorted array is then passed to the unique array, which returns a string array that contains the set of unique category values—and since the unique filter doesn’t change the order of the values it processes, the results remain sorted by the previous filter.

Or, to put it more directly, this is an instruction to the ng-repeat directive to generate a set of unique category names, enumerate each of them, assign the current value to a variable called item, and generate an a element for each value.

Tip I could have reversed the filters and achieved the same effect. The difference would be that the orderBy filter would be operating on an array of strings, rather than product objects (because that’s what the unique filter produces as its result). The orderBy filter is designed to operate on objects, but you can sort strings by using this incantation: orderBy:’toString()’. Don’t forget the quotes; otherwise, AngularJS will look for a scope property called toString, rather than invoking the toString method.

1.3. Handling the Click Event

I used the ng-click directive on the a elements so that I can respond when the user clicks of the buttons. AngularJS provides a set of built-in directives, which I describe in Chapter 11, that make it easy to call controller behaviors in response to events. As its name suggests, the ng-click directive specifies what AngularJS should do when the click event is triggered, as follows:

<a ng-click=”selectCategory()” class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”>

{{item}}

</a>

There are two a elements in the app.html file. The first is static and creates the Home button, which I will use to display all of the products in all of the categories. For this element, I have set the ng-click directive so that it calls a controller behavior called selectCategory with no arguments. I’ll create the behavior shortly, but for now, the important thing to note is that for the other a element—the one to which the ng-repeat directive has been applied—I have set up the ng-click directive so that it calls the selectCategory behavior with the value of the item variable as the argument. When the ng-repeat directive generates an a element for each unique category, the ng-click directive will be automatically configured such that the selectCategory behavior will be passed the category for the button, such as selectCategory(‘Category #1’), for example.

2. Selecting the Category

Clicking the category buttons in the browser doesn’t have any effect at the moment because the ng-click directive on the a elements is set up to call a controller behavior that isn’t defined. AngularJS doesn’t complain when you try to access a nonexistent behavior or data value on the scope on the basis that it might be defined at some point in the future. This can make debugging a little frustrating because typos don’t result in errors, but the flexibility that this approach gives is generally useful, as I explain in Chapter 13 when I describe how controllers and their scopes work in more depth.

2.1. Defining the Controller

I need to define a controller behavior called selectCategory in order to respond to the user clicking the category buttons. I don’t want to add the behavior to the top-level sportsStoreCtrl controller, which I am reserving for behaviors and data that are required for the entire application. Instead, I am going to create a new controller that will be used just by the product listing and category views. Listing 6-6 shows the contents of the controllers/ productListControllers.js file, which I added to the project in order to define the new controller.

Tip You may be wondering why I used a more specific name for the controller’s file than for the one that contains filters. The reason is that filters are more generic and readily reused in other parts of the application or even other applications, whereas the kind of controller I am creating in this section tends to be tied to specific functionality. (This isn’t true for all controllers, however, as you’ll see in Chapters 15-17 when I show you how to create custom directives.)

Listing 6-6. The Contents of the productListControllers.js File

angular.module(“sportsStore”)

.controller(“productListCtrl”, function ($scope, $filter) {

var selectedCategory = null;

$scope.selectCategory = function (newCategory) {

selectedCategory = newCategory;

}

$scope.categoryFilterFn = function (product) {

return selectedCategory == null ||

product.category == selectedCategory;

}

});

I call the controller method on the sportsStore module that is defined in the app.html file (remember that one argument to the angular.module method means find an existing module, while two arguments means create a new one).

The controller is called productListCtrl, and it defines a behavior called selectCategory, matching the name of the behavior that the ng-click directives in Listing 6-5. The controller also defines categoryFilterFn, which takes a product object as its argument and returns true if no category has been selected or if a category has been selected and the product belongs to it—this will be useful shortly when I add the controller to the view.

Tip Notice that the selectedCategory variable is not defined on the scope. It is just a regular JavaScript variable, and that means it cannot be accessed from directives or data bindings in the view. The effect I have created is that the selectCategory behavior can be called to set the category, and the categoryFilterFn can be used to filter the product objects, but details of which category has been selected remains private. I won’t be relying on this feature in the SportsStore applications—I just wanted to draw your attention to how controllers (and most other kinds of AngularJS components) can be selective about what public services and data they provide.

2.2. Applying the Controller and Filtering the Products

I have to apply the controller to the view using the ng-controller directive so that the ng-click directive is able to invoke the selectCategory behavior. Otherwise, the scope for the elements that contain the ng-click directive would be the one created by the top-level sportsStoreCtrl controller that doesn’t contain the behavior. You can see the changes I have made to do this in Listing 6-7.

Listing 6-7. Applying a Controller in the app.html File

<!DOCTYPE html>

<html ng-app=”sportsStore”>

<head>

<title>SportsStore</title>

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

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

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

<script>

angular.module(“sportsStore”, [“customFilters”]);

</script>

<script src=”controllers/sportsStore.js”></script>

<script src=”filters/customFilters.js”></script>

<script src=”controllers/productListControllers.js”></script>

</head>

<body ng-controller=”sportsStoreCtrl”>

<div class=”navbar navbar-inverse”>

<a class=”navbar-brand” href=”#”>SPORTS STORE</a>

</div>

<div class=”panel panel-default row” ng-controller=”productListCtrl”>

<div class=”col-xs-3″>

<a ng-click=”selectCategory()”

class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”>

{{item}}

</a>

</div>

<div class=”col-xs-8″>

<div class=”well”

ng-repeat=”item in data.products | filter:categoryFilterFn”>

<h3>

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

<span class=”pull-right label label-primary”>

{{item.price | currency}}

</span>

</h3>

<span class=”lead”>{{item.description}}</span>

</div>

</div>

</div>

</body>

</html>

I have added a script element to import the productListControllers.js file and applied the ng-controller directive for the productListCtrl controller on the part of the view that contains both the list of categories and the list of products.

Placing the ng-controller directive for the productListCtrl controller within the scope of the one for the sportsStoreCtrl controller means I can take advantage of controller scope inheritance, which I explain in detail in Chapter 13. The short version is the scope for the productListCtrl inherits the data.products array and any other data and behaviors that sportsStoreCtrl defines, which are then passed on to the view for the productListCtrl controller, along with any data or behaviors that it defines. The benefit of using this technique is that it allows you to limit the scope of controller functionality to the part of the application where it will be used, which makes it easier to perform good unit tests (as described in Chapter 25) and prevents unexpected dependencies between components in the application.

There is one other change in Listing 6-7: I changed the configuration of the ng-repeat directive that generates the product details, like this:

<div class=”well” ng-repeat=”item in data.products | filter:categoryFilterFn“>

One of the built-in filters that AngularJS provides is called, confusingly, filter. It processes a collection and selects a subset of the objects it contains. I describe filters in Chapter 14, but the technique I am using here is to specify the name of the function defined by the productListCtrl controller. By applying the filter to the ng-repeat directive that creates the product details, I ensure that only the products in the currently selected category are displayed, as illustrated by Figure 6-9.

3. Highlighting the Selected Category

The user can click the category buttons to filter the products, but there is no visual feedback to show which category has been selected. To address this, I am going to selectively apply the Bootstrap btn-primary CSS class to the category button that corresponds to the selected category. The first step is to add a behavior to the controller that will accept a category and, if it is the selected category, return the CSS class name, as shown in Listing 6-8.

Tip Notice how I am able to chain together method calls on an AngularJS module. This is because the methods defined by the Module return the Module, creating what is commonly referred to as a fluent API.

Listing 6-8. Returning the Bootstrap Class Name in the productListControllers.js File

angular.module(“sportsStore”)

.constant(“productListActiveClass”, “btn-primary”)

.controller(“productListCtrl”, function ($scope, $filter, productListActiveClass) {

var selectedCategory = null;

$scope.selectCategory = function (newCategory) {

selectedCategory = newCategory;

}

$scope.categoryFilterFn = function (product) {

return selectedCategory == null ||

product.category == selectedCategory;

}

$scope.getCategoryClass = function (category) {

return selectedCategory == category ? productListActiveClass : “”;

}

});

I don’t want to embed the name of the class in the behavior code, so I have used the constant method on the Module object to define a fixed value called productListActiveClass. This will allow me to change the class that is used in one place and have the change take effect wherever it is used. To access the value in the controller, I have to declare the constant name as a dependency, like this:

.controller(“productListCtrl”, function ($scope, $filter, productListActiveClass) {

I can then use the productListActiveClass value in the getCategoryClass behavior, which simply checks the category it receives as an argument and returns either the class name or the empty string.

The getCategoryClass behavior may seem a little odd, but it is going to be called by each of the category navigation buttons, each of which will pass the name of the category it represents as the argument. To apply the CSS class, I use the ng-class directive, which I have applied to the app.html file in Listing 6-9.

Listing 6-9. Applying the ng-class Directive to the app.html File

<div class=”col-xs-3″>

<a ng-click=”selectCategory()”

class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”

ng-class=”getCategoryClass(item)”>

{{item}}

</a>

</div>

The ng-class attribute, which I describe in Chapter 11, will add the element to which it has been applied to the classes returned by the getCategoryClass behavior. You can see the effect this creates in Figure 6-10.

4. Adding Pagination

The last feature I am going to add in this chapter is pagination, such that only a certain number of product details are displayed at once. I don’t really have enough data to make pagination terribly important, but it is a common requirement and worth demonstrating. There are three steps to implementing pagination: modify the controller so that the scope tracks the pagination state, implement filters, and update the view. I explain each step in the sections that follow.

4.1. Updating the Controller

I have updated the productListCtrl controller to support pagination, as shown in Listing 6-10.

Listing 6-10. Updating the Controller to Track Pagination in the productListControllers.js File

angular.module(“sportsStore”)

.constant(“productListActiveClass”, “btn-primary”)

.constant(“productListPageCount”, 3)

.controller(“productListCtrl”, function ($scope, $filter,

productListActiveClass, productListPageCount) {

var selectedCategory = null;

$scope.selectedPage = 1;

$scope.pageSize = productListPageCount;

$scope.selectCategory = function (newCategory) {

selectedCategory = newCategory;

$scope.selectedPage = 1;

}

$scope.selectPage = function (newPage) {

$scope.selectedPage = newPage;

}

$scope.categoryFilterFn = function (product) {

return selectedCategory == null ||

product.category == selectedCategory;

}

$scope.getCategoryClass = function (category) {

return selectedCategory == category ? productListActiveClass : ‘”‘;

}

$scope.getPageClass = function (page) {

return $scope.selectedPage == page ? productListActiveClass : “”;

}

});

The number of products shown on a page is defined as a constant called productListPageCount, which I have declared as a dependency of the controller. Within the controller I define variables on the scope that expose the constant value (so I can access it in the view) and the currently selected page. I have defined a behavior, selectPage, that allows the selected page to be changed and another, getPageClass, that is designed for use with the ng-class directive to highlight the selected page, much as I did with the selected category earlier.

Tip You might be wondering why the view can’t access the constant values directly, instead of requiring everything to be explicitly exposed via the scope. The answer is that AngularJS tries to prevent tightly coupled components, which I described in Chapter 3. If views could access services and constant values directly, then it would be easy to end up with endless couplings and dependencies that are hard to test and hard to maintain.

4.2. Implementing the Filters

I have created two new filters to support pagination, both of which I have added to the customFilters.js file, as shown in Listing 6-11.

Listing 6-11. Adding Filters to the customFilters.js File

angular.module(”customFilters”, [])

.filter(“unique”, function () {

return function (data, propertyName) {

if (angular.isArray(data) && angular.isString(propertyName)) {

var results = [];

var keys = {};

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

var val = data[i][propertyName];

if (angular.isUndefined(keys[val])) {

keys[val] = true;

results.push(val);

}

}

return results;

} else {

return data;

}

}

})

.filter(“range”, function ($filter) {

return function (data, page, size) {

if (angular.isArray(data) && angular.isNumber(page) && angular.isNumber(size)) {

var start_index = (page – 1) * size;

if (data.length < start_index) {

return [];

} else {

return $filter(“limitTo”)(data.splice(start index), size);

}

} else {

return data;

}

}

})

.filter(“pageCount”, function () {

return function (data, size) {

if (angular.isArray(data)) {

var result = [];

for (var i = 0; i < Nath.ceil(data.length / size) ; i++) {

result.push(i);

}

return result;

} else {

return data;

}

}

});

The first new filter, called range, returns a range of elements from an array, corresponding to a page of products. The filter accepts arguments for the currently selected page (which is used to determine the start index of range) and the page size (which is used to determine the end index).

The range filter isn’t especially interesting, other than I have built on the functionality provided by one of the built-in filters, called limitTo, which returns up to a specified number of items from an array. To use this filter, I have declared a dependency on the $filter service, which lets me create and use instances of filter. I explain how this works in detail in Chapter 14, but the key statement from the listing is this one:

return $filter(“limitTo”)(data.splice(start_index), size);

The result is that I use the standard JavaScript splice method to select part of the data array and then pass it to the limitTo filter to select no more than the number of items that can be displayed on the page. The limitTo filter ensures that there are no problems stepping over the end of the array and will return fewer items if the specified number isn’t available.

The second filter, pageCount, is a dirty—but convenient—hack. The ng-repeat directive makes it easy to generate content, but it works only on data arrays. You can’t, for example, have it repeat a specified number of times. My filter works out how many pages an array can be displayed in and then creates an array with that many numeric values. So, for example, if a data array can be displayed in three pages, then the result from the pageCount filter would be an array containing the values 1, 2, and 3. You’ll see why this is useful in the next section.

4.3. Updating the View

The last step to implement pagination is to update the view so that only one page of products is displayed and to provide the user with buttons to move from one page to another. You can see the changes I have made to the app.html file in Listing 6-12.

Listing 6-12. Adding Pagination to the app.html File

<!DOCTYPE html>

<html ng-app=”sportsStore”>

<head>

<title>SportsStore</title>

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

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

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

<script>

angular.module(“sportsStore”, [“customFilters”]);

</script>

<script src=”controllers/sportsStore.js”></script>

<script src=”filters/customFilters.js”></script>

<script src=”controllers/productListControllers.js”></script>

</head>

<body ng-controller=”sportsStoreCtrl”>

<div class=”navbar navbar-inverse”>

<a class=”navbar-brand” href=”#”>SPORTS STORE</a>

</div>

<div class=”panel panel-default row” ng-controller=”productListCtrl”>

<div class=”col-xs-3″>

<a ng-click=”selectCategory()”

class=”btn btn-block btn-default btn-lg”>Home</a>

<a ng-repeat=”item in data.products | orderBy:’category’ | unique:’category'”

ng-click=”selectCategory(item)” class=” btn btn-block btn-default btn-lg”

ng-class=”getCategoryClass(item)”>

{{item}}

</a>

</div>

<div class=”col-xs-8″>

<div class=”well”

ng-repeat=

“item in data.products | filter:categoryFilterFn | range:selectedPage:pageSize”>

<h3>

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

<span class=”pull-right label label-primary”>

{{item.price | currency}}

</span>

</h3>

<span class=”lead”>{{item.description}}</span>

</div>

<div class=”pull-right btn-group”>

<a ng-repeat=

“page in data.products | filter:categoryFilterFn | pageCount:pageSize”

ng-click=”selectPage($index + 1)” class=”btn btn-default”

ng-class=”getPageClass($index + 1)”>

{{$index + 1}}

</a>

</div>

</div>

</div>

</body>

</html>

The first change is to the ng-repeat directive that generates the product list so that the data is passed through the range filter to select the products for the current page. The details of the current page and the number of products per page are passed to the filter as arguments using the values I defined on the controller scope.

The second change is the addition of the page navigation buttons. I use the ng-repeat directive to work out how many pages the products in the currently selected category requires and pass the result to the pageCount filter, which then causes the ng-repeat directive to generate the right number of page navigation buttons. The currently selected page is indicated through the ng-class directive, and the page is changed through the ng-click directive.

You can see the result in Figure 6-11, which shows the two pages required to display all of the products. There are not enough items in the fake data for any one category to require multiple pages, but the effect is evident.

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 *