Async Angular filter

How to compute the filter's result asynchronously.

A typical AngularJs filter is a synchronous function that should execute very quickly. For example, we can write a camel-case conversion filter using Lodash#camelCase

1
2
3
4
angular.module('filters', [])
.filter('camelCase', function () {
return _.camelCase;
});

The AngularJS assumes that your filter function returns the same result if given the same arguments. Thus it caches the filtered results to avoid calling the filter. What if you wanted to compute the result of the filter asynchronously? The filter cannot return a promise, it must return some value, at least an undefined. The best it can hope is to recompute the value again in the future and return something else. Such filters are called stateful, and they require property $stateful to be set to true on the returned function.

script.js
1
2
3
4
5
6
7
8
9
10
angular.module('App', [])
.filter('details', function () {
function detailsFilter(input) {
console.log('details for', input);
// by default return nothing
}
detailsFilter.$stateful = true;
return detailsFilter;
})
.controller('ctrl', function () {});

The markup input field is

1
<input ng-model="something">: {{something | details}}

The function detailsFilter will be called on each digest cycle. You can see the working example below. Notice a new console message from detailsFilter appears if you type in the input box (which changes the value of the variable something). It also appears if you click the button that forces the digest cycle to execute.

Let us introduce an async component to the computed value. Imagine we want to call the server with the input and display the returned result. Or for simplicity, compute the result after 1 second. The result will be just a concatenation of the input. Here is a our code - we now must cache the computed value ourselves to avoid redoing the work on every digest cycle.

script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
angular.module('App', [])
.filter('details', function ($timeout) {
var cached = {};
function detailsFilter(input) {
console.log('details for', input);
if (input) {
if (input in cached) {
// avoid returning a promise!
return typeof cached[input] === 'string' ? cached[input] : undefined;
} else {
cached[input] = $timeout(function () {
cached[input] = input + input;
console.log('generated result for', input);
}, 1000);
}
}
}
detailsFilter.$stateful = true;
return detailsFilter;
})
.controller('ctrl', function ($scope) {
$scope.something = 'foo';
});

For demo purposes, I start with initial string foo.

As you can see the string is computed only once. Now try typing in the input box and see how when a new value is computed, the text changes but only after 1 second. If you click the "force digest" button, the cached value is returned without any time out delay.

In practice, such filter allows to easily add dynamic server information to any template without manual scope / controller coding. Filter functions allow passing additional parameters from the HTML template, which allows great flexibility. For example, here is a filter for fetching Github user details.

1
2
3
4
5
6
Github user <input ng-model="username">
<br>
full name {{username | details:'name'}}
blog {{username | details:'blog'}}
<br>
<img ng-src="{{username | details:'avatar_url'}}" width="100" height="100" >

Notice the markup simplicity, and here is the corresponding filter code that caches fetched user information object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.filter('details', function ($http) {
var cached = {};
var githubApiUrl = 'https://api.github.com/users/';
function detailsFilter(input, field) {
console.log('details for github user', input, field);
if (input) {
if (input in cached) {
// avoid returning a promise!
return typeof cached[input].then !== 'function' ?
cached[input][field] : undefined;
} else {
cached[input] = $http({
method: 'GET',
url: githubApiUrl + input
}).success(function (info) {
cached[input] = info;
console.log('generated result for', info);
});
}
}
}
detailsFilter.$stateful = true;
return detailsFilter;
})

You can see the filter in action below. Change the entered username to something else to see the UI update.

Happy hacking