Using Filters with AngularJS: Creating Custom Filters

You are not restricted to the built-in filters and can create your own to process data in ways that are specific to your applications. In this section I will show you how to create three different filters: one that formats a single data value, one that processes an array of objects, and one that builds on functionality provided by other filters.

1. Creating a Filter That Formats a Data Value

Filters are created by the Module.filter method, which takes two arguments: the name of the filter that will be created and a factory function that creates the worker function that will undertake the actual work. To demonstrate how to create a filter, I have added a new JavaScript file called customFilters.js to the angularjs folder. Listing 14-18 shows the contents of the new file.

Listing 14-18. The Contents of the customFilters.js File

angular.module(“exampleApp”)

 

.filter(“labelCase”, function () {

return function (value, reverse) {

if (angular.isString(value)) {

var intermediate = reverse ? value.toUpperCase() : value.toLowerCase();

return (reverse ? intermediate[0].toLowerCase() :

intermediate[0].toUpperCase()) + intermediate.substr(1);

} else {

return value;

}

};

});

Note Notice that I have used the angular.module method with only one argument in this example. This retrieves a previously defined module for further configuration—in this case, the module defined in the main filters.html file. By retrieving the module, I am able to call the filter method to supplement the exampleApp module, even though my code is in a separate file.

The filter I have created is called labelCase, and it formats a string so that only the first letter is capitalized. The worker function I defined accepts two arguments: The first is the value that is to be filtered (which will be provided by AngularJS when the filter is applied), and the second allows the use of the filter to be reversed so that the first letter is lowercase and the rest of the string is capitalized.

Tip Notice that I used the angular.isString method to check that the value my filter is formatting is really a string. Although I differentiated between using single-value and collection filters in this chapter, there is no real difference when writing either kind of filter, and it is always worth checking that you have received the kind of data you are expecting, which can happen when your filter is misapplied in a directive expression. For this filter, I simply return the data value unmodified if it isn’t a string, but you may prefer to generate an error that will be detected during testing.

Before I can apply the filter, I need to add a script element to the filters.html file to bring the code contained in the customFilters.js file into the main document, as shown in Listing 14-19.

Listing 14-19. Adding a script Element for the customFilters.js File to the filters.html File

<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) {

// …statements omitted for brevity…

});

</script>

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

</head>

I have to put the script element for the customFilters.js file after the one that defines the exampleApp module, since the code in the JavaScript files relies on the module already being defined. The only other change I have to make is to apply the filter to the markup. In Listing 14-20, you can see how I have applied the filter to the Name and Category columns of the table in the filters.html file.

Listing 14-20. Applying a Custom Filter to the filters.html File

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

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

<td>{{p.category \ labelCase:true }}</td>

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

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

</tr>

I have not specified a configuration option when applying the filter to the name property, which means that AngularJS will pass null as the value for the second argument to the filter worker function. I have written the filter so that a false or null value for the second value means that the default behavior is used, and I recommend you take the same approach for your custom filters because it makes them easier to use. I have specified the configuration option true when applying the filter to the category property, which will invert the case transformation applied by the filter. You can see the effect of both filters in Figure 14-16.

2. Creating a Collection Filter

The process for creating a filter that operates on a collection of objects is just the same but is worth demonstrating anyway. In this section, I am going to build a skip filter that removes a specified number of items from the head of the array—something that isn’t that useful on its own but which I’ll build on later in the chapter. Listing 14-21 shows the additions I made to define the skip filter in the customFilters.js file.

Listing 14-21. Defining a Collection Filter in the customFilters.js File

angular.module(“exampleApp”)

.filter(“labelCase”, function () {

return function (value, reverse) {

if (angular.isString(value)) {

var intermediate = reverse ? value.toUpperCase() : value.toLowerCase();

return (reverse ? intermediate[0].toLowerCase() :

intermediate[0].toUpperCase()) + intermediate.substr(1);

} else {

return value;

}

};

})

.filter(“skip”, function () {

return function (data, count) {

if (angular.isArray(data) && angular.isNumber(count)) {

if (count > data.length || count < 1) {

return data;

} else {

return data.slice(count);

}

} else {

return data;

}

}

});

In the worker function, I check to see that the data is an array and that I have received a numeric value for the count argument. I perform some bounds checking to make sure that the filter can perform the requested transformation on the array, and if all is well, I use the built-in JavaScript slice method to skip over the specified number of objects. In Listing 14-22, you can see how I have applied the skip filter to the expression for the ng-repeat directive in the filters.html file. (I also removed the labelCase filter from the table columns.)

Listing 14-22. Applying a Custom Collection Filter in the filters.html File

<tr ng-repeat=”p in products | skip:2 | limitTo: 5″>

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

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

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

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

</tr>

I have chained the skip and limitTo filters to emphasize that custom filters are used in just the same way as the built-in ones. The overall effect of these two filters is to skip over the first two items and then select the next five, as shown in Figure 14-17.

3. Building on Existing Filters

In this section I am going to combine the functionality of skip and limitTo in a single filter. As Listing 14-20 demonstrated, it is easy to use chaining to apply these filters together, but I want to demonstrate how you can build on existing filter functionality without having to duplicate any code. In Listing 14-23, you can see the additions I made to the customFilters.js file to define the take filter.

Listing 14-23. Defining the take Filter in the customFilters.js File

angular.module(“exampleApp”)

.filter(“labelCase”, function () {

// …statements omitted for brevity…

})

.filter(“skip”, function () {

// …statements omitted for brevity…

})

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

return function (data, skipCount, takeCount) {

var skippedData = $filter(“skip”)(data, skipCount);

return $filter(“limitTo”)(skippedData, takeCount);

}

});

My take filter doesn’t implement a transformation itself and doesn’t even check to see what kind of data it is working with. Instead, it relies on the skip and limitTo filters, which perform their own validation and apply their transformations as though they were being used directly.

In the listing, my filter factory function declares a dependency on the $filter service, which provides access to all of the filters that have been defined in the module. These filters are accessed and invoked in the worker function by name, like this:

var skippedData = $filter(“skip”)(data, skipCount);

This statement invokes the skip filter within the worker function, and I store the processed data collection by assigning it to a regular JavaScript variable. I repeat the process with the limitTo filter, allowing me to create a filter that builds on other filters. In Listing 14-24, you can see how I have applied this filter to the expression for the ng-repeat directive in the filters.html file.

Listing 14-24. Applying the take Filter in the filters.html File

<tr ng-repeat=”p in products | take:2:5″>

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

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

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

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

</tr>

Tip This is the first time I have used a filter whose worker function expects more than one configuration argument; you can see that I provide the values for the arguments by separating them with colons. AngularJS takes care of processing the values and passing them to the worker function.

My take filter doesn’t offer any real advantage over the filters it builds on, but the example demonstrates how easy it is to build filters that do add value without duplicating functionality you have created elsewhere.

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 *