Angular: infinite digest loop in filter - angularjs

I'm writing a custom Angular filter that randomly capitalizes the input passed to it.
Here's the code:
angular.module('textFilters', []).filter('goBananas', function() {
return function(input) {
var str = input;
var strlen = str.length;
while(strlen--) if(Math.round(Math.random())) {
str = str.substr(0,strlen) + str.charAt(strlen).toUpperCase() + str.substr(strlen+1);
}
return str;
};
});
I call it in my view like so:
<a class='menu_button_news menu_button' ng-href='#/news'>
{{"News" | goBananas}}
</a>
It works, but in my console I'm seeing a rootScope:infdig (infinite digest) loop.
I'm having some trouble understanding why this is happening and what I can do to resolve it. If I understand correctly, this is due to the fact that there are more than 5 digest actions called by this function. But the input is only called once by the filter, right?
Any help appreciated.

The problem is that the filter will produce a new result every time it is called, and Angular will call it more than once to ensure that the value is done changing, which it never is. For example, if you use the uppercase filter on the word 'stuff' then the result is 'STUFF'. When Angular calls the filter again, the result is 'STUFF' again, so the digest cycle can end. Contrast that with a filter that returns Math.random(), for example.
The technical solution is to apply the transformation in the controller rather than in the view. However, I do prefer to transform data in the view with filters, even if the filter applies an unstable transformation (returns differently each time) like yours.
In most cases, an unstable filter can be fixed by memoizing the filter function. Underscore and lodash have a memoize function included. You would just wrap that around the filter function like this:
.filter('myFilter', function() {
return _memoize(function(input) {
// your filter logic
return result;
});
});

Since digest will continue to run until consistent state of the model will be reached or 10 iterations will run, you need your own algorithm to generate pseudo-random numbers that will return the same numbers for the same strings in order to avoid infinite digest loop. It will be good if algorithm will use character value, character position and some configurable seed to generate numbers. Avoid using date/time parameters in such algorithm. Here is one of possible solutions:
HTML
<h1>{{ 'Hello Plunker!' | goBananas:17 }}</h1>
JavaScript
angular.module('textFilters', []).
filter('goBananas', function() {
return function(input, seed) {
seed = seed || 1;
(input = input.split('')).forEach(function(c, i, arr) {
arr[i] = c[(c.charCodeAt(0) + i + Math.round(seed / 3)) % 2 ? 'toUpperCase' : 'toLowerCase']();
});
return input.join('');
}
});
You can play with seed parameter to change a bit an algorithm. For example it may be $index of ngRepeat
Plunker: http://plnkr.co/edit/oBSGQjVZjhaIMWNrPXRh?p=preview

An alternative, if you want the behaviour to be truly random, is to do deal with the randomness only once during linking by creating a seed, and then use a seeded random number generator in the actual filter:
angular.module('textFilters', []).filter('goBananas', function() {
var seed = Math.random()
var rnd = function () {
var x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
return function(input) {
var str = input;
var strlen = str.length;
while(strlen--) if(Math.round(rnd())) {
str = str.substr(0,strlen) + str.charAt(strlen).toUpperCase() + str.substr(strlen+1);
}
return str;
};
});

Related

Should a custom filter modify / update its input?

Say I have a custom filter like so:
app.filter('custom', function () {
return function (input, search) {
const ret = {};
// find matches in input, given search
ret[key] = input[key] // etc etc
return ret;
});
here is the HTML, that works with the filter:
<div class="row" ng-repeat="(promptId, q) in myHash | custom:searchText">
what I believe I need to do, is set myHash in the controller to the ret value from the custom filter?
Is that the right thing to do, if so, how can I do that?
In other words, should I do something like this:
app.filter('custom', function ($scope) {
return function (input, search) {
const ret = {};
// find matches in input, given search
ret[key] = input[key] // etc etc
return $scope.myHash = ret; // do not do this LOL
});
"[What] I believe I need to do, is set myHash in the controller to the ret value from the custom filter?"
No, that would be wrong. A filter should not modify its input. It should produce a new value derived from the input (that is, its function parameters).
From the AngularJS documentation on filters (emphasis added by me):
"The filter function should be a pure function, which means that it should always return the same result given the same input arguments and should not affect external state."
As far as I can see, you're already doing it the right way (in your first code example).
Surprisingly, the following worked.
before:
<div class="row" ng-repeat="(promptId, q) in myHash | custom:searchText">
after:
<div class="row" ng-repeat="(promptId, q) in (myFilteredHash = (myHash | custom:searchText))">
So now, I have a second variable in my controller's $scope called myFilteredHash.
You could probably pass the $scope to the filter and then set myFilteredHash to the result.
I think the only thing you shouldn't do, is set the original value to the filtered value, because then you basically lose all your data! On top of that, you might set yourself up for an (infinite) digest loop.

Strongloop promise inside loop

I am trying to call a loopback find function inside of a for loop, passing in a value from the iteration into the loopback function. The main issue of the code can be represented by the following:
for (var a = 0; a < $scope.countries.length; a++) {
$scope.getEmFacPurElec($scope.countries[a], 'ton/kWh', 'CO2e').then(function(result) {
emFacPurElecToUse = $scope.emFacPurElecs;
}
And here is the function being called:
$scope.getEmFacPurElec = function (country, unit, ghgType) {
var defer = $q.defer();
$scope.emFacPurElecs = [];
$scope.emFacPurElecs = Country.emFacPurElecs({
id: country.id,
filter: {
where: {
and: [
{unit: unit},
{ghgType: ghgType}
]
}
}
});
defer.resolve('Success getEmFacPurElec');
return defer.promise;
};
The problem is that the loopback promise function is called and then returned undefined which means that it moves to the next iteration of the for loop before getting the value to assign to emFacPurElecToUse. I need to do some more calculations with that variable for that country before moving to the next country.
I have looked at using $q.all as a possible solution and also using array.map as per http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html (Rookie mistake #2: WTF, how do I use forEach() with promises?), but I just cannot figure out how to pull it all together to make it work. Should I be using a forEach instead?
I also saw this link angular $q, How to chain multiple promises within and after a for-loop (along with other similar ones) but I do not have multiple promises that I need to process inside the for loop. I need to retrieve the value of one emFacPurElecs for that country, do some work with it, then move to the next country. I feel I am close but I just cannot get my head around how I would code this particular functionality. Any help is greatly appreciated.
It seems to me that you do have multiple promises to process inside your for loop, as you say "I need to do some more calculations with that variable for that country before moving to the next country." This should all be done with in the promise chain that I've suggested - calcEmFacPurElec.
$scope.calcEmFacPurElec = function (country, unit, ghgType) {
$scope.getEmFacPurElec(country, unit, ghgType).then(function(countryEmFacPurElecs) {
// do something with countryEmFacPurElecs
return countryEmFacPurElecs;
}
$scope.getEmFacPurElec = function (country, unit, ghgType) {
var defer = $q.defer();
defer.resolve(Country.emFacPurElecs({
id: country.id,
filter: {
where: {
and: [
{unit: unit},
{ghgType: ghgType}
]
}
}
}); );
return defer.promise;
};
Hopefully the above is a pointer in the right direction!
When you want to carry out a promise chain on an array of items, then as you have identified, Promise.all (using whatever promises implementation you require) is what you want. .all takes in an array of Promises, so in your for loop you can do:
var promises = [];
for (var a = 0; a < $scope.countries.length; a++) {
promises.push($scope.calcEmFacPurElec($scope.countries[a], 'ton/kWh', 'CO2e')); // new promise chain that does all of the work for that country
}
$q.all(promises).then(function(arrayofCountryEmFacPurElecs) {console.log('all countries completed')});

How to display the calculations from a filter in AngularJS?

I wrote an app to convert between decimal values and negabinary.
http://dev.golightlyplus.com/playground/negabinary/
I wrote a custom filter so I could do
{{decimal | negabinary}}
The code for it being..
var negabinaryApp = angular.module('negabinaryApp', []);
negabinaryApp.filter('negabinary', function() {
return function (decimal) {
if (isNaN(decimal)) return "not a number";
var negabinary = [];
var base = -2;
var remainder;
while(decimal != 0) {
remainder = decimal % base;
decimal = Math.ceil(decimal / base);
negabinary.push(remainder >= 0 ? remainder : -remainder);
}
return negabinary.reverse().join('');
}
});
What I'd like to be able to do is to also show the calculations on the page.
I could create an array of the calculations for each cycle through the while loop. But how do I then bind them to the HTML? Or is there a better way to go about this?
Thanks.
The actual functionality of your particular filter is distracting from the purpose of the question. To simplify, your question is: "how can I display that output of a filter that takes a string, and parses it into HTML"?
This would apply equally to your case as it would to a case where you just wrap the text in <pre>, for example.
The key, is to bind the output to ng-bind-html:
<div ng-bind-html="decimal | negabinary">
Here's a simple (and useless) example:
.filter("strongify", function(){
return function(str){
return "<strong>" + str + "</strong>";
}
});
which can be used similarly:
<div ng-bind-html="name | strongify">
Here's a plunker that breaks the text into paragraphs of <p>: http://plnkr.co/edit/RN5TqwNRRjMjynwInNyn?p=preview
Note, that you will also need to add ngSanitize dependency or otherwise do $sce.trustAsHtml on the output.

nest ng-repeat generates much more items than expected

Main idea:
I want to combine bootstrap grid system with angular ng-repeat
Method:
I used filter to reformat the json data (an array with a lot of objects), like the codepen project:
http://codepen.io/maggiben/pen/sfCnq
```
filter('listToMatrix', function() {
return function listToMatrix(list, elementsPerSubArray) {
var matrix = [], i, k;
console.log("hellowrld")
for (i = 0, k = -1; i < list.length; i++) {
if (i % elementsPerSubArray === 0) {
k++;
matrix[k] = [];
}
matrix[k].push(list[i]);
}
return matrix;
};
});
And here is my jade page code:
My controller code: To get the news.
The problem is I got much more items on the page:
This error means angular doesn't manage to finish a digest cycle. The reason for it comes from your filter: every time angular applies the filter to your list, a new matrix is created, so angular will keep invoking the filter until what it returns is the same as the previous iteration (which never happens).
To fix it, you could either track the items of your matrix using ng-repeat ... track by items.someProperty, so after two consecutives calls to your filter, angular will detect that this someProperty has not changed and will finish the cycle.
Another way to fix it would be to cache the result of your filter for a given list, so the next time angular invoyes your filter with the same list, you would return a pointer to the same matrix.
For more lecture you can refer to this question (same problem): Angular filter works but causes "10 $digest iterations reached"

$rootScope:infdig error caused by filter?

I'm doing a filter on an array in an ng-repeat.
div.row(ng-repeat="log in logs | prepare_log | orderBy: 'log.created'")
In the prepare_log filter I'm doing this:
value.stack_trace = angular.fromJson(value.stack_trace)
If I change it to this:
value.stack_trace = angular.fromJson(value.stack_trace).reverse()
I get the infdig error.
I don't know if it's relevant, but I'm iterating over the stack_trace property in an inner ng-repeat.
Any ideas what I'm doing wrong here?
You are causing an infinite $digest loop because you are changing the model during the digest loop.
What happens behind the scenes is this:
ng-repeat parses the collection expression to figure out what rows need to be 'stamped' out and instantiates a watcher to get notified when the collection changes
every time the filter is run, you change one of the items by assigning a new value to value.stack_trace, triggering the watcher of the ng-repeat to pick this up and starting over and over again
Angular detects the loop and aborts
To solve the problem avoid changing the model in your filter.
Hope that helps!
because angular will always trigger digest one more time to make sure there is no more changes. in every digest cycle prepare_log filter will be called and return a value. if return value is the same from last one, it means no more changes and the application state is stabilized, thus angular does not have to trigger an extra digest.
but in your filter, value.stack_trace will be reversed in every digest cycle, thus the application state is never stabilized, which causing infdig (infinite digest) error.
Solving infdig errors caused by filters in an ngRepeat can be cumbersome and annoying. When you just want a quick fix, it is often enough to articulate as long as the input array doesn't change in order or size, give me the same result.
This is made a lot easier if the models you're dealing with all have a unique id property.
In that case, we like to deploy a generic filter stabilization approach:
angular
.module( "app" )
.factory( "stabilize", stabilizeFactory );
/**
* Generalization of a filter stabilization approach.
* As long as the input contains the same elements (identified by their `id`) and they are in the same order,
* the same result array is returned, thus preventing infdig errors.
*/
function stabilizeFactory() {
return function stabilize( filterFunc ) {
return function stableFilter() {
var array = arguments[ 0 ];
if( !array || !array.length ) {
return array;
}
var stabilizationId = idString( array );
if( array.$$stable ) {
if( array.$$stableId === stabilizationId ) {
return array.$$stable;
}
}
array.$$stable = filterFunc.apply( arguments );
array.$$stableId = stabilizationId;
return array.$$stable;
};
};
function idString( array ) {
return array.reduce( function appendKey( id, element ) {
return id + element.id;
}, "" );
}
}
To use this, just wrap your own filter function in stabilize(), like so:
angular
.module( "app" )
.filter( "myFilter", myFilterProvider);
/** #ngInject */
function myFilterProvider( stabilize ) {
return stabilize( myFilter);
function myFilter( array ) {
if( !array || !array.length ) {
return array;
}
return array.filter( function( element ) {
return element.something === "foo";
}
);
}
}

Resources