Reputation: 33
I have been working on AngularJS project recently and came over an interesting problem while trying to create a filter which is using data loaded via AJAX request.
First about the problem:
AngularJS filter is a synchronous piece of code (function) that returns a string which is inserted into your DOM. And in most of the cases it works perfectly fine e.g. following filter that capitilizes first letter:
angular.module('myApp.filters', []).filter('capitilize', function() {
return function (word) {
return word.charAt(0).toUpperCase() + word.substr(1);
}
});
And this works great. Now the question is what if I can't return the desired result right away? Say I need to load some data via AJAX request to get the desired result. If I make an AJAX request my return statement will return an empty result before I get my data. So really the question is how do I notify filter to update itself when my data is loaded?
Solution:
It turned out that the solution was right there in front of me, but it took me some time to figure out how the magic is happening. Say I need a filter that retrieves artist's biography based on their name (yeah, a little bit crazy example but it proves the point):
angular.module('myApp.filters', []).filter('biography', function($q, $http) {
var pending = {};
return function (artist) {
if ( !(artist in pending) ) {
pending[artist] = null;
$http.get('http://developer.echonest.com/api/v4/artist/biographies?api_key=FILDTEOIK2HBORODV&name='
+ artist + '&format=json&results=1&start=0&license=cc-by-sa')
.then(function(response){
pending[artist] = response.data.response.biographies[0].text;
});
}
return pending[artist] || '';
}
});
It works, but how? I made a get request, got my result, but how does it force filter to update itself. The key here is the angular's $q (A promise/deferred implementation inspired by Kris Kowal's Q.)
from angular documentation:
$q is integrated with the $rootScope.Scope Scope model observation mechanism in angular, which means faster propagation of resolution or rejection into your models and avoiding unnecessary browser repaints, which would result in flickering UI.
This means that whenever the promise is resolved it causes the update, in fact here is the code from angular:
function done(status, response, headersString, statusText) {
if (cache) {
if (isSuccess(status)) {
cache.put(url, [status, response, parseHeaders(headersString), statusText]);
} else {
// remove promise from the cache
cache.remove(url);
}
}
resolvePromise(response, status, headersString, statusText);
if (!$rootScope.$$phase) $rootScope.$apply();
}
Hope this was helpful for you. Here is the example, keep in mind that the example is making cross domain ajax calls, you will need to disable cross domain policy of your browser:
IMPORTANT: Keep in mind not to overwhelm filter with same ajax request, otherwise you might end up with this:
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
Upvotes: 0
Views: 3099
Reputation: 106
thank you for that post, at least I knew I'm not completely wrong. I have a very similar scenario:
1) a simple translate filter
module.filter('translate', ['Localization', function (localization) {
var translateFilter = function (key) {
return localization.get(key) || "[" + key + "]";
};
translateFilter.$stateful = true;
return translateFilter;
}]);
2) a localization service which stores a JS dictionary. That is updated via XHR. That means that there are multiple possible states of the dictionary:
My problem was that the filter result wasn't updated (re-rendered) after the XHR call ended, even though a digest was performed. I had to add
translateFilter.$stateful = true;
to make it work. Even changing filter dependency from service
to value
did not help. Someone might find this helpful, or event better, tell me, what I was doing wrong :-)
Upvotes: 2