AngularJS filters in Depth
AngularJS filters come in handy when formatting the data for display and as such they are most often used along with AngularJS expressions.
Currency Filter
As the name indicates, it formats a number as currency. It formats the given value to two decimal spaces also in addition to adding a currency symbol. Pipe (|) symbol is used for applying a filter inside an expression. Armed with that knowledge let’s dive in and apply the currency filter to a number.
<body ng-app><div>{{ 12345.67 | currency }}</div> </body>
currency filter accepts the symbol for the currency and the default is $. We can use the (:) symbol to pass parameter to a filter as follows.
<body ng-app><div>{{ "12345.67" | currency:"USD" }}</div> </body>
It adheres the locale settings and if we include the specific locale js file for AngularJs, it will use the currency symbol from that locale. The following example is run after including the locale file for UK (Ukrainian). If both locale file and the argument symbol are provided, the argument will take precedence.
<body ng-app><div>{{ "12345.67" | currency }}</div> </body>
Since we are comfortable with the currency filter usage, we will dive right into its source code.
function currencyFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
return function(amount, currencySymbol){
if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM;
return formatNumber(amount, formats.PATTERNS, formats.GROUP_SEP, formats.DECIMAL_SEP, 2).
replace(/\u00A4/g, currencySymbol);
};
}
Filter is an injectable service and so it should return an object and is returning a function (since function is an object in javascript). $locale is a built in AngularJS service. Different locales could be supported in AngularJS application by using the corresponding locale file. We have already demonstrated one by including uk specific locale file. Switching the locale on the fly is not supported (seems so) in AngularJS as of yet. $locale is identified by its id (like en-us) and has two dictionaries holding the localization details. These two dictionary properties are DATETIME_FORMATS and NUMBER_FORMATS.
If we have not provided the currency symbol parameter to the currency filter, the filter function will retrieve CURRENCY_SYM value from the NUMBER_FORMATS dictionary mentioned previously. It then uses the formatNumber utility to format the number properly.
It seems like all the heavy work is done by the formatNumber utility method.
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
// rest of the code omitted
// return formatted string
}
The first parameter to formatNumber is the number to be formatted. The second parameter named pattern expects the same dictionary/object present in the PATTERNS array of NUMBER_FORMATS on $locale. Negative amounts are displayed in different ways for different locales. In UK and France, negative amount is shown by placing the negative symbol before the currency symbol and the amount. In denmark, negative symbol is placed before the number but after the symbol. In Netherlands, the negative symbol is placed after the amount and in US the negative amount is placed inside the parentheses. Here’s the currency formatting applied for FranceDenmark and US. These complex rules are taken care of by four properties named negPre, posPre, negSuf and posSuf on the pattern parameter. The other three parameters are self explanatory and we will go through examples later.
Date Filter
It formats the date based on the given argument. Its only argument could be one of the predefined localizable strings or a pattern composed from the date and time elements. The predefined localizable strings include ‘medium’, ‘short’, ‘fullDate’, ‘longDate’, ‘mediumDate’, ‘shortDate’, ‘mediumTime’, and ‘shortTime’. If this parameter is omitted, mediumDate will be selected as default.
<body ng-app><div>{{ 1288323623006 | date }}</div><div>{{ 1288323623006 | date:"mediumDate" }}</div><div>{{ 1288323623006 | date:"medium" }}</div><div>{{ 1288323623006 | date:"short" }}</div><div>{{ 1288323623006 | date:"fullDate" }}</div><div>{{ 1288323623006 | date:"longDate" }}</div><div>{{ 1288323623006 | date:"shortDate" }}</div><div>{{ 1288323623006 | date:"mediumTime" }}</div><div>{{ 1288323623006 | date:"shortTime" }}</div></body>
Localizable strings adhere to the localization rules and localization is as easy as including the corresponding localized file. In the above example we have used the milliseconds (in Number) for demonstration and this could be milliseconds as string, ISO 8601 formatted dates, or a Date object itself.
<body ng-app><div>{{ 1288323623006 | date:"h" }}</div><div>{{ 1288323623006 | date:"h hours" }}</div><div>{{ 1288323623006 | date:"h 'hours'" }}</div><div>{{ 1288323623006 | date:"h 'hours' mm:ss" }}</div><div>{{ 1288323623006 | date:"h o' clock'" }}</div><div>{{ 1288323623006 | date:"h 'o'' clock'" }}</div>
outputs:
9
9 9our23
9 hours
9 hours 10:23
9 o clock
9 o' clock
</body>
You can use a set of characters to fine tune the date formatting further. If you need to include other string literal you can place them inside single quotes (’). If single quote ’ character need to be displayed, then you have to escape it using doubling the character ’.
orderBy and filter Filters
The filter named as filter is used for selecting a subset of an array in AngularJS. The two requirements for performing a filtering are the array to filter and a predicate. The predicate in the case of AngularJS could be a string, an object, or a function.
The rest of this setion will walkthrough creating a small application and applying different filters. Let’s quickly create the HTML boiler plate code required for the sample application.
<!DOCTYPE html><html lang="en" ng-app="app"><head><meta charset="utf-8"><title>iTunes Explorer</title><script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script><script src="http://code.angularjs.org/1.0.7/angular-resource.js"></script><script src="app.js"></script><link rel="stylesheet" type="text/css" href="bootstrap/css/bootstrap.css"></head><body><div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"> <table class="table"><thead><tr><th>Artist Name</th><th>Track</th><th>Genre</th><th>Price</th> </tr></thead><tbody></tbody></table></div></div></div></body></html>
HTML file is very simple, it references AngularJS, Bootstrap, and our to be written AngularJS application files. Also incuded is the AngularJS Resources library to interact with the iTunes API.
Using Bootstrap layout styles we have created a container for our table. The table will list the data obtained from our application controller named MediaCtrl since the entire HTML content is made in to an AngularJS application using the ngApp directive.
var app = angular.module("app", ['ngResource'])
app.controller("MediaCtrl", function ($scope, $resource){
console.log("Got hold of resource", $resource);
var url = "https://itunes.apple.com/search";
var SearchResult = $resource(url, {term:"spice+girls", callback: 'JSON_CALLBACK'}, {get:{method:'JSONP'}});
var result = SearchResult.get(function(){
console.log("Got result", result);
$scope.result = result;
});
});
The only dependency for our application is ngResource and our app module takes care of fulfilling that dependency as we have already placed the JavaScript file for ngResource. MediaCtrl will use the resource service to get the results for the term spice girls through the Apple iTunes API. The get method on that resource will be called as JSONP method to overcome browser restrictions on getting data from external domains. Finally, once the data is ready, we will bind the result on the scope so that view can display the data. We have to make the following changes in the view and then we are ready for applying the filters.
<table class="table"><thead><tr><th>Artist Name</th><th>Track</th><th>Genre</th><th>Price</th> </tr></thead><tbody><tr ng-repeat="r in result.results"><td>{{r.artistName}}</td><td>{{r.trackName}}</td><td>{{r.primaryGenreName}}</td><td>{{r.trackPrice}}</td></tr></tbody></table>
Bringing Order
We can use orderBy filter to provide the sorting functionality for the table we have just created. In its simplest form orderBy filter accepts a string for evaluation and uses that result for sorting. The string passed is usually the property name of an object inside the array. Let’s make our columns sortable.
<div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"> <table class="table"><thead><tr><th><a href="" ng-click="sortBy='artistName'">Artist Name</a></th><th><a href="" ng-click="sortBy='trackName'">Track</a></th><th>Genre</th><th>Price</th> </tr></thead><tbody><tr ng-repeat="r in result.results | orderBy:sortBy"><td>{{r.artistName}}</td><td>{{r.trackName}}</td><td>{{r.primaryGenreName}}</td><td>{{r.trackPrice}}</td></tr></tbody></table></div></div></div>
We use our own variable called sortBy to track which column needs ordering. In the click event of the table header’s each link we set this variable to the corresponding column name. sortBy is then passed as first argument to the orderBy filter applied to our relevant data. This works for single click but there is no way to reverse the order once the column is ordered. We have to use another parameter reverse with boolean value for that functionality.
<div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"><pre>Sorting expression = {{sortBy}}; reverse = {{reverse}}</pre><table class="table"><thead><tr><th><a href="" ng-click="sortBy='artistName'; reverse=!reverse;">Artist Name</a></th><th><a href="" ng-click="sortBy='trackName'; reverse=!reverse;">Track</a></th><th><a href="" ng-click="sortBy='primaryGenreName'; reverse=!reverse;">Genre</a></th><th><a href="" ng-click="sortBy='trackPrice'; reverse=!reverse;">Price</a></th> </tr></thead><tbody><tr ng-repeat="r in result.results | orderBy:sortBy:reverse"><td>{{r.artistName}}</td><td>{{r.trackName}}</td><td>{{r.primaryGenreName}}</td><td>{{r.trackPrice}}</td></tr></tbody></table></div></div></div>
In adition to setting the expression for sorting, ngClick now sets the value of reverse to a boolean value opposite to the current one. Now the sort functions somewhat as intended.
We can specify an array as the orderBy expression also. In this way we can have more control over the sorting capabilities. Coupled with negating the string expression facility to reverse the array, we can implement sorting by multiple columns.
<div class="span12" ng-controller="MediaCtrl"><pre>{{sortExpression}}</pre><table class="table"><thead><tr><th><a href="" ng-click="setSortExpression('artistName')">Artist Name</a></th><th><a href="" ng-click="setSortExpression('trackName')">Track</a></th><th><a href="" ng-click="setSortExpression('primaryGenreName')">Genre</a></th><th><a href="" ng-click="setSortExpression('trackPrice')">Price</a></th> </tr></thead><tbody><tr ng-repeat="r in result.results | orderBy:sortExpression"><td>{{r.artistName}}</td><td>{{r.trackName}}</td><td>{{r.primaryGenreName}}</td><td>{{r.trackPrice}}</td></tr></tbody></table></div>
ng-click of each anchor inside the header will call setSortExporession function using the corresponding column name. The rest of the markup is very similar except the sortExpression is supposed to be an array instead of a string. The interesting parts happen inside the app.js file.
var app = angular.module("app", ['ngResource'])
app.controller("MediaCtrl", function ($scope, $resource){
console.log("Got hold of resource", $resource);
var url = "https://itunes.apple.com/search";
var SearchResult = $resource(url, {term:"spice+girls", callback: 'JSON_CALLBACK'}, {get:{method:'JSONP'}});
var result = SearchResult.get(function(){
console.log("Got result", result);
$scope.result = result;
});
$scope.sortExpression = [];
$scope.setSortExpression = function(expression){
console.log(expression);
var indexOfExpression = $scope.sortExpression.indexOf(expression);
var reversedExpression = "-" + expression;
var indexOfReversedExpression = $scope.sortExpression.indexOf(reversedExpression);
if ( indexOfExpression > -1) {
$scope.sortExpression[indexOfExpression] = reversedExpression;
}else if(indexOfReversedExpression > -1){
console.log("need to remove expression", indexOfReversedExpression);
$scope.sortExpression.splice(indexOfReversedExpression, 1);
}else{
$scope.sortExpression.push(expression);
};
}
});
Note that the sortExpression is an empty array initially. When the setSortExpression is called with the column name, it does the following checking and tasks.
1. If the column name is not present in the sortExpression array then just add to it.
2. If the column name is present in the sortExpression array, retrieve it and prepend a negative sign (-) to it
3. If the prepended with negative sign column name is present in the array just remove that thing
Now we have a more feature rich ordering facility in place.
Filtering
Filter is used for selecting a subset of a give array. The requirements for a filter are an array to be filtered and the predicate based on which the array need to be filtered. The predicate could be as simple as a string and let’s try filtering using a string at first.
<div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"><input type="text" ng-model="searchTerm"><table class="table"><thead><tr><th><a href="" ng-click="setSortExpression('artistName')">Artist Name</a></th><th><a href="" ng-click="setSortExpression('trackName')">Track</a></th><th><a href="" ng-click="setSortExpression('primaryGenreName')">Genre</a></th><th><a href="" ng-click="setSortExpression('trackPrice')">Price</a></th> </tr></thead><tbody><tr ng-repeat="r in result.results | filter:searchTerm | orderBy:sortExpression"><td>{{r.artistName}}</td><td>{{r.trackName}}</td><td>{{r.primaryGenreName}}</td><td>{{r.trackPrice}}</td></tr></tbody></table></div></div></div>
Giving a full blown filtering to our application is as easy as introduing a new model (searchTerm here) and applying the filter to the array model passing the model value as argument. The drawback (or advantage) of this method is it matches the records based on comparing the search term with each and every property of the objects in the array. If we need to fine tune the filtering process we can pass an object instead of a simple string.
<div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"><input type="text" ng-model="searchTerm.$"><table class="table"><thead><tr><th><a href="" ng-click="setSortExpression('artistName')">Artist Name</a><br><input type="text" ng-model="searchTerm.artistName"></th><th><a href="" ng-click="setSortExpression('trackName')">Track</a><br><input type="text" ng-model="searchTerm.trackName"></th><th><a href="" ng-click="setSortExpression('primaryGenreName')">Genre</a><br><input type="text" ng-model="searchTerm.primaryGenreName"></th><th><a href="" ng-click="setSortExpression('trackPrice')">Price</a><br><input type="text" ng-model="searchTerm.trackPrice"></th> </tr></thead><tbody><tr ng-repeat="r in result.results | filter:searchTerm | orderBy:sortExpression"><td>{{r.artistName}}</td><td>{{r.trackName}}</td><td>{{r.primaryGenreName}}</td><td>{{r.trackPrice}}</td></tr></tbody></table></div></div></div>
Now the search tern is an object with properties matching those on the listed columns and the list is filtered based on the values inside this object. Dollar ($) symbol has a special meaning of match all the properties and it is used for searching across all of the properties.
json, lowercase, and uppercase Filters
The rest of the built in AngularJS filters are trivial and their functionality is evident from their names itself. Let’s apply a bunch of them to our sample application.
<div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"><pre>searchTerm Object: {{searchTerm | json}}</pre><input type="text" ng-model="searchTerm.$"><table class="table"><thead><tr><th><a href="" ng-click="setSortExpression('artistName')">Artist Name</a><br><input type="text" ng-model="searchTerm.artistName"></th><th><a href="" ng-click="setSortExpression('trackName')">Track</a><br><input type="text" ng-model="searchTerm.trackName"></th><th><a href="" ng-click="setSortExpression('primaryGenreName')">Genre</a><br><input type="text" ng-model="searchTerm.primaryGenreName"></th><th><a href="" ng-click="setSortExpression('trackPrice')">Price</a><br><input type="text" ng-model="searchTerm.trackPrice"></th> </tr></thead><tbody><tr ng-repeat="r in result.results | limitTo:10 | filter:searchTerm | orderBy:sortExpression"><td>{{r.artistName | lowercase}}</td><td>{{r.trackName }}</td><td>{{r.primaryGenreName | uppercase}}</td><td>{{r.trackPrice | number:1}}</td></tr></tbody></table></div></div></div>
searchTerm is now passed through the json filter so as to display it in a friendly manner for debugging purpose. Artist name and Genre are now lowercase and uppercase respectively by applying the filters of same names. The result we display are also limited to 10 through limitTo filter. Finally the number filter specifies how many decimal points need to be displayed.
Creating A Filter
Creating a filter in AngularJS is almost trivial and we will make a minimal truncate filter to restrict the display of track name to the specific number of characters. Let’s start coding how it should be used.
<div class="container" style="margin-top:100px;"><div class="row"><div class="span12" ng-controller="MediaCtrl"><pre>searchTerm Object: {{searchTerm | json}}</pre><input type="text" ng-model="searchTerm.$"><table class="table"><thead><tr><th><a href="" ng-click="setSortExpression('artistName')">Artist Name</a><br><input type="text" ng-model="searchTerm.artistName"></th><th><a href="" ng-click="setSortExpression('trackName')">Track</a><br><input type="text" ng-model="searchTerm.trackName"></th><th><a href="" ng-click="setSortExpression('primaryGenreName')">Genre</a><br><input type="text" ng-model="searchTerm.primaryGenreName"></th><th><a href="" ng-click="setSortExpression('trackPrice')">Price</a><br><input type="text" ng-model="searchTerm.trackPrice"></th> </tr></thead><tbody><tr ng-repeat="r in result.results | filter:searchTerm | orderBy:sortExpression"><td>{{r.artistName | lowercase}}</td><td>{{r.trackName | truncate:5 }}</td><td>{{r.primaryGenreName | uppercase}}</td><td>{{r.trackPrice | number:1}}</td></tr></tbody></table></div></div></div>
We will then code the bare minumum required for registering a filter.
app.filter("truncate", function(){ return function(text, length){ return "it works"; } });
Now the track name displays the hard code “it works” text instead of the track name and it’s just a matter of putting the logic in its place for completing the filter.
app.filter("truncate", function(){
return function(text, length){
if (text) {
var ellipsis = text.length > length ? "..." : "";
return text.slice(0, length) + ellipsis;
};
return text;
}
});
The filter should be registered with the filter. Module has a filter method to register a filter of given name. The filter method accepts the name of the filter and a factory function that can return the actual filter function.
getting hold of Filters from Javascript
So far we have applied filters inside the AngularJS expression and sometimes we have to apply the filters inside our application file. If we append “Filter” to the registered filter name and name the controller parameter so, AngularJS will inject the specified filter as shown below.
app.controller("MediaCtrl", function ($scope, $resource, limitToFilter){
console.log("Got hold of filter", limitToFilter);
var url = "https://itunes.apple.com/search";
var SearchResult = $resource(url, {term:"spice+girls", callback: 'JSON_CALLBACK'}, {get:{method:'JSONP'}});
var result = SearchResult.get(function(){
console.log("Got result", result);
result.results = limitToFilter(result.results, 10);
$scope.result = result;
});
$scope.sortExpression = [];
$scope.setSortExpression = function(expression){
console.log(expression);
var indexOfExpression = $scope.sortExpression.indexOf(expression);
var reversedExpression = "-" + expression;
var indexOfReversedExpression = $scope.sortExpression.indexOf(reversedExpression);
if ( indexOfExpression > -1) {
$scope.sortExpression[indexOfExpression] = reversedExpression;
}else if(indexOfReversedExpression > -1){
console.log("need to remove expression", indexOfReversedExpression);
$scope.sortExpression.splice(indexOfReversedExpression, 1);
}else{
$scope.sortExpression.push(expression);
};
}
});
We have specified limitToFilter as a dependecy for MediaCtrl and AngularJS will fulfil that request. Then we have used the limitTo filter inside the javascript file to restrict the result displayed in the view.
Example 12: Sample application with all the filters and custom filter through JavaScript applied
Conclusion
Filters is one of the easy to grasp features in AngularJS. Filters take care of the formatting requirements of the models in the views. Although a few filters come bundled with AngularJS, it is easy to create new ones. Chainability of filters comes handy when we have to apply filters one after another.