Using Filters with AngularJS: Filtering Collections

Filtering collections works by using the same basic techniques as filtering single data values but generally requires more care and consideration to get the right result. The AngularJS library includes three built-in collection filters—which I describe in the following sections—and, of course, you can create your own custom filters, a process that I describe in the “Creating Custom Filters” section later in this chapter.

1. Limiting the Number of Items

The limitTo filter restricts the number of items taken from an array of data objects, which can be useful when working with a layout that can accommodate only a certain number of items. Listing 14-9 shows the changes I made to the filters.html file to demonstrate the limitTo filter.

Listing 14-9. Using the limitTo Filter in the filters.html File

<html ng-app=”exampleApp”>

<head>

<title>Filters</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

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

$scope.products = [

{ name: “Apples”, category: “Fruit”, price: 1.20, expiry: 10 },

{ name: “Bananas”, category: “Fruit”, price: 2.42, expiry: 7 },

{ name: “Pears”, category: “Fruit”, price: 2.02, expiry: 6 },

{ name: “Tuna”, category: “Fish”, price: 20.45, expiry: 3 },

{ name: “Salmon”, category: “Fish”, price: 17.93, expiry: 2 },

{ name: “Trout”, category: “Fish”, price: 12.93, expiry: 4 },

{ name: “Beer”, category: “Drinks”, price: 2.99, expiry: 365 },

{ name: “Wine”, category: “Drinks”, price: 8.99, expiry: 365 },

{ name: “Whiskey”, category: “Drinks”, price: 45.99, expiry: 365 }

]

$scope.limitVal = “5”;

$scope.limitRange = [];

for (var i = (0 – $scope.products.length);

i <= $scope.products.length; i++) {

$scope.limitRange.push(i.toString());

}

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h3>

Products

<span class=”label label-primary”>{{products.length}}</span>

</h3>

</div>

<div class=”panel-body”>

Limit: <select ng-model=”limitVal”

ng-options=”item for item in limitRange”></select>

</div>

<div class=”panel-body”>

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

<thead>

<tr>

<td>Name</td>

<td>Category</td>

<td>Expiry</td>

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

</tr>

</thead>

<tbody>

<tr ng-repeat=”p in products | limitTo:limitUal”>

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

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

<td>{{p.expiry}}</td>

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

</tr>

</tbody>

</table>

</div>

</div>

</body>

</html>

Tip Notice that I have removed the localization file for this example for brevity.

The most important change in the filters.html file is the application of the limitTo filter to the products array in the expression I defined for the ng-repeat directive, as follows:

<tr ng-repeat=”p in products | limitTo:limitVal”>

The collections that operate on filters are applied in the same way as those applied to single values. The limitTo filter is configured by specifying the number of items that should be used from the source array. In this example,

I have specified that the limit is set by the value of the limitVal property, which is defined in the controller factory function:

$scope.limitVal = “5”;

In other words, I have used the limitTo filter to restrict the ng-repeat directive so that it operates only on the first five objects in the products array, as illustrated by Figure 14-9.

Like all filters, limitTo doesn’t modify the data in the scope; it affects only the data that is passed to the ng-repeat directive. You can see this in the figure where the counter next to the Products title still reads 9 (because it is generated through an inline binding to products.length) even though the ng-repeat directive has received only five objects from the array.

Tip The limitTo filter will also operate on string values, treating each character as an object in an array.

I specified the value for the limitTo filter as a variable to demonstrate what happens when you specify negative values. To that end, the filters.html markup contains a select element whose ng-options attribute is set to an array of values that I create like this:

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

$scope.limitRange.push(i.toString());

}

There are nine data objects in the products array, which means that the select element contains option elements from -9 through to 9. If you configure the limitTo filter with a positive number—such as 5, in the figure—then the filter selects the first five objects from the array. However, if you select a negative value, such as -5, for example, then the filter selects the last five objects in the array. You can see the effect in Figure 14-10.

Tip You don’t have to worry about going out of bounds. The limitTo filter will return all of the objects in the array if you specify a number that is greater than the size of the array.

2. Selecting Items

The confusingly named filter filter is used to select objects from an array. The selection criteria can be specified as an expression, as a map object used to match property values, or using a function. Listing 14-10 shows a simple selection.

Listing 14-10. Selecting items in the filters.html file

<tr ng-repeat=”p in products | filter:{category: ‘Fish’}”>

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

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

<td>{{p.expiry}}</td>

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

</tr>

I have used the map object approach in this example, specifying that I want to select those items whose category property is Fish. If you filter using a function, the selected items will be those for which the function returns true, as shown in Listing 14-11.

Listing 14-11. Selecting Items in the filters.html File

<html ng-app=”exampleApp”>

<head>

<title>Filters</title>

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

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

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

<script>

angular.module(“exampleApp”, [])

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

$scope.products = [

{ name: “Apples”, category: “Fruit”, price: 1.20, expiry: 10 },

{ name: “Bananas”, category: “Fruit”, price: 2.42, expiry: 7 },

{ name: “Pears”, category: “Fruit”, price: 2.02, expiry: 6 },

{ name: “Tuna”, category: “Fish”, price: 20.45, expiry: 3 },

{ name: “Salmon”, category: “Fish”, price: 17.93, expiry: 2 },

{ name: “Trout”, category: “Fish”, price: 12.93, expiry: 4 },

{ name: “Beer”, category: “Drinks”, price: 2.99, expiry: 365 },

{ name: “Wine”, category: “Drinks”, price: 8.99, expiry: 365 },

{ name: “Whiskey”, category: “Drinks”, price: 45.99, expiry: 365 }

];

$scope.limitVal = “5”;

$scope.limitRange = [];

for (var i = (0 – $scope.products.length) ;

i <= $scope.products.length; i++) {

$scope.limitRange.push(i.toString());

}

$scope.selectItems = function (item) {

return item.category == “Fish” || item.name == “Beer”;

};

});

</script>

</head>

<body ng-controller=”defaultCtrl”>

<div class=”panel panel-default”>

<div class=”panel-heading”>

<h3>

Products

<span class=”label label-primary”>{{products.length}}</span>

</h3>

</div>

<div class=”panel-body”>

Limit: <select ng-model=”limitVal”

ng-options=”item for item in limitRange”></select>

</div>

<div class=”panel-body”>

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

<thead>

<tr>

<td>Name</td>

<td>Category</td>

<td>Expiry</td>

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

</tr>

</thead>

<tbody>

<tr ng-repeat=”p in products | filter:selectltems”>

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

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

<td>{{p.expiry}}</td>

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

</tr>

</tbody>

</table>

</div>

</div>

</body>

</html>

In this example, I defined a scope behavior called selectItems. This behavior is invoked for each item in the collection and is passed each object in turn. My implementation returns true if the category property is Fish or if the name property is Beer. Using a behavior to provide the filter with a function allows for more complex selections than is possible using an expression.

3. Sorting Items

The most complex built-in filter is orderBy, which sorts the objects in an array. In Listing 14-12, you can see how I have applied the orderBy filter to the example.

Listing 14-12. Applying the orderBy Filter in the filters.html File

<tr ng-repeat=”p in products | orderBy:’price'”>

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

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

<td>{{p.expiry}}</td>

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

This is the simplest way to order objects—by specifying the name of a property by which the objects should be sorted. In this case I have specified the price property, which has the effect shown in Figure 14-11.

3.1. Setting the Sort Direction

By specifying just a property name, I am implicitly asking the filter to sort the objects in ascending order. I can make the order explicit by using the + or – character, as shown in Listing 14-13.

Listing 14-13. Explicitly Setting the Sort Direction in the filters.html File

<tr ng-repeat=”p in products | orderBy:’-price’“>

By prefixing the property name with a minus sign (-) I specify that the objects should be sorted in descending order of their price property, as shown in Figure 14-12. Specifying a plus sign (+) is equivalent to no prefix at all and has the effect of applying an ascending sort.

3.2. Sorting by Function

The reason you have to be careful when specifying a property name as a literal string is because the orderBy filter will also sort using a function, which allows for sorting in ways that are not directly tied to single property values.

In Listing 14-14, you can see how I have defined such a function, which performs a sort based on multiple properties.

Listing 14-14. Defining a Sorting Function in the filters.html File

<script>

angular.module(“exampleApp”, [])

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

$scope.products = [

{ name: “Apples”, category: “Fruit”, price: 1.20, expiry: 10 },

{ name: “Bananas”, category: “Fruit”, price: 2.42, expiry: 7 },

{ name: “Pears”, category: “Fruit”, price: 2.02, expiry: 6 },

{ name: “Tuna”, category: “Fish”, price: 20.45, expiry: 3 },

{ name: “Salmon”, category: “Fish”, price: 17.93, expiry: 2 },

{ name: “Trout”, category: “Fish”, price: 12.93, expiry: 4 },

{ name: “Beer”, category: “Drinks”, price: 2.99, expiry: 365 },

{ name: “Wine”, category: “Drinks”, price: 8.99, expiry: 365 },

{ name: “Whiskey”, category: “Drinks”, price: 45.99, expiry: 365 }

];

$scope.myCustomSorter = function (item) {

return item.expiry < 5 ? 0 : item.price;

}

});

</script>

Functions used for sorting are passed an object from the data array and return an object or value that will be used for comparison during sorting. In this function, I return the value of the price property unless the value of the expiry property is less than 5, in which case I return zero. The effect of this function is that items with a small expiry value will be placed near the start of the data array for ascending sorts. In Listing 14-15, you can see how I have applied the function to the orderBy filter.

Listing 14-15. Applying the Search Function in the filters.html File

<tr ng-repeat=”p in products | orderBy:myCustomSorter”>

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

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

<td>{{p.expiry}}</td>

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

</tr>

Notice that I don’t surround the name of the function with quotes, as I did when I specified a property name as a literal string. You can see the effect the function has in Figure 14-13.

3.3. Sorting with Multiple Predicates

AngularJS sorting functions are a little odd. Rather than being asked to compare two objects and determine their relative ranking, you are asked to return a value that the orderBy filter will use to perform the ranking. This means you can end up with something that only approximates the intended effect when you assign weight to values of different properties, as I did in the previous example. I wanted to put the objects with the smallest expiry values at the top of the table—which happened—but the orderBy filter didn’t perform any further ranking.

Fortunately, you can configure the orderBy filter to use an array of property names or functions that will be used in sequence to order objects. If two objects have the same value for the first property or function in the array, then the orderBy filter will consider the second value or function, continuing until it can rank the data objects or runs out of properties/functions to try. In Listing 14-16, you can see how I have applied an array in the filters.html file.

Listing 14-16. Using an Array for Sorting with the orderBy Filter in the filters.html File

<tr ng-repeat=”p in products | orderBy:[myCustomSorter, ‘-price’]”>

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

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

<td>{{p.expiry}}</td>

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

</tr>

I have applied the myCustomSorter function from the previous section, followed by the price property in descending order. The myCustomSorter function assigns the same sorting value for objects with a small expiry value, so using the array lets me sort those objects relative to one another, as shown in Figure 14-14.

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 *