Why do I get an infinite digest error here? - angularjs

I need to create groups of things that come as a flat array, so I can open and close grid rows using a CSS grid system.
Here's how my HTML looks:
<div ng-repeat="row in items | GroupsOf: 3" class="row">
[show stuff]
</div>
And here's the filter I wrote to support this:
.filter('GroupsOf', function(){
//Takes an array of things and groups them into
// sub-arrays with numPerGroup things in each
return function(things, numPerGroup){
var i, group = -1, groups = []
for(i = 0; i < things.length; i++){
//if it's a new group:
if(i % numPerGroup === 0){
groups.push([])
group++
}
groups[group].push(things[i])
}
return groups//
}
})
Although things render as expected, I'm getting infinite digest error when this runs, and therefore not everything gets wired up properly.
Why do I get that error, and how do I fix the filter to work w/o erring?
I'd really prefer to do this as a filter rather than grouping the data in the controller, but I'll go the later route if someone can explain to me why it's just not achievable as a filter.

If you wanted to use $filter in the controller( which is certainly more performant and fixes the infinite $digest loop issue) you can do :
angular.module('myApp', [])
.controller('myCtrl', function($scope, $filter) {
$scope.items = [
'one','two','three',
'unos','dos','tres',
'uno','due','tre'
]
$scope.grouped = $filter('GroupsOf')($scope.items , 3)
console.log($scope.grouped)
})
.filter('GroupsOf', function(){
//Takes an array of things and groups them into
// sub-arrays with numPerGroup things in each
return function(things ,numPerGroup){
var i, group = -1, res = []
for(i = 0; i < things.length ; i++){
//if it's a new group:
if(i % numPerGroup === 0){
res.push([])
group++
}
res[group].push(things[i])
}
return res
}
})
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myCtrl">
<div ng-repeat="row in grouped" class="row">
{{row}}
</div>
</div>
looking at the infdig documentation it seems your problem is caused by
.. binding to a function which generates a new array every time it is called.
that should be the reason $digest never finds a clean state.

Related

AngularJS ng repeat performance

I have a ng-repeat on my html view. The array it's repeating is populated each time a user selects a checkbox. It works like a filter way.
However, I am having issues with performance. It seems to create the DOM elements again and thus doing this there is a 1-1.5 second 'freeze' delay before the results are populated on the user interface again.
The array it's repeating isnt that big - probably around 50-60 entries. Each entry in the array has two objects which do have a lot of properties. Will this affect the performance? From what I've read it seems to be because it's creating the DOM elements again.
I have tried using track by $index, which speeds it up drastically but this causes problems on the div boxes I'm displaying. Text is on the wrong results, gets mixed up etc. I have also tried using track by ($index + item). No luck - same problem. I've also tried using track by item.id - but this has the same effect of not using track by - slow.
Is there anything I can do to optimize this? Or do I just bite the bullet?
Here is my code below:
<div ng-if="$ctrl.hasDataProcessed() && $ctrl.resultsAvailable()">
<div class="acca-builder-content">
<div class="acca-builder-header" style="border: 1px solid #1393ED;">{{"RESULTS" | translate}} ({{$ctrl.accaBuilderResultsCount}})</div>
<ul class="tips-list-group-matches">
<li ng-repeat="result in $ctrl.accaBuilderResults | orderBy: $ctrl.getSort" ng-class="{'match-has-link': $ctrl.canViewMatch(result.match)}" class="tip-list-group-match">
<tf-competition-header ng-if="result.match.CompMasterID" competition="result.match"></tf-competition-header>
<match-header match="result.match" tracking-screen="Tips"></match-header>
</li>
</ul>
</div>
</div>
</div>
And within the controller:
var buildAccaResultsFromFilter = function () {
var results = [];
var tips = ctrl.tips;
for (var i = 0; i < tips.length; i++) {
var tip = tips[i];
if(valueInFilter("COMPETITIONS", tip.match.CompID) &&
valueInFilter("DATES", tip.match.MatchDateConverted) &&
valueInFilter("SHOW", tip.tip.TipType)) {
results.push(tip);
}
}
if(results.length > 0) {
ctrl.accaBuilderResults = results;
ctrl.accaBuilderResultsCount = results.length;
ctrl.resultsFound = true;
} else {
clearAccaBuilderResults();
ctrl.resultsFound = false;
}
};
// Function called when a checkbox is clicked
ctrl.onCheckboxChange = function (option, item) {
item.checkState = !item.checkState;
if(item.checkState) {
addToFilter(option.optionKey, item.textKey);
}
else {
removeFromFilter(option.optionKey, item.textKey);
}
if(option.onChange) {
option.onChange(item.checkState, item.checkId);
}
if(ctrl.canBuildAccaResults()) {
buildAccaResultsFromFilter();
} else {
clearAccaBuilderResults();
}
};
// Checks if a value is present within the filter by it's key
var valueInFilter = function (filterKey, value) {
return ctrl.filter[filterKey].includes(value);
};
ctrl.resultsAvailable = function () {
return ctrl.accaBuilderResults && ctrl.accaBuilderResults.length > 0;
};
ctrl.hasDataProcessed = function () {
return ctrl.tips && ctrl.competitions;
};
Without seeing some code it is tough to optimize.
You can try and eliminate any ng-show and ng-hide and use ng-if only. It will remove a lot of watchers if you have those present.
Additionally you can use the syntax below in your directives if you don’t need two-way binding. This will also remove a watcher. The more watchers you can remove in ng-repeat, the better.
{{:: expression }}

AngularJS Filter throws infdig error when it creates new array

i am about to bang my head to walls. i thought i had an understanding of how angular works (filters too). but i just cant find the problem about my filter. it causes infdig. and i even dont change source array in filter.
(function () {
angular.module('project.filters').filter('splitListFilter', function () {
return function (data, chunk) {
if(!data || data.length === 0){
return data;
}
var resultArray = [];
for (var i = 0, j = data.length; i < j; i += chunk) {
resultArray.push(data.slice(i, i + chunk));
}
return resultArray;
};
});
})();
i have lists where i need to split data to x columns. it is complicated to solve with limitTo.
(limitTo: $index*x | limitTo: $last ? -z : -x)
it causes a dirty template file. so i decided to create a filter which splits an array to groups.
[1,2,3,4,5,6,7,8] -> [[1,2,3],[4,5,6],[7,8]]
so i can easily use it in my template.
Can u help me about what causes infdig in this filter?
Edit: the error message itself looks strange with some numbers in that don't appear anywhere in the code, which can be seen at http://plnkr.co/edit/pV1gkp0o5KeimwPlEMlF
10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: regularInterceptedExpression","newVal":23,"oldVal":20}],[{"msg":"fn: regularInterceptedExpression","newVal":26,"oldVal":23}],[{"msg":"fn: regularInterceptedExpression","newVal":29,"oldVal":26}],[{"msg":"fn: regularInterceptedExpression","newVal":32,"oldVal":29}],[{"msg":"fn: regularInterceptedExpression","newVal":35,"oldVal":32}]]
HTML Template
<div class="row" ng-repeat="chunk in docProfile.SysMedicalInterests | splitListFilter: 3">
<div class="col-md-4" ng-repeat="medInterest in chunk">
<label style="font-weight:normal;">
<input type="checkbox" value="{{medInterest.ID}}" ng-click="docProfile.saveInterest(medInterest.ID)" ng-checked="docProfile.isMedChecked(medInterest.ID)"> {{medInterest.Name}}
</label>
</div>
</div>
Controller Code
var me = this;
me['SysMedicalInterests'] = null;
var loadMedicalInterests = function(){
var postData = { 'Data': me['data']['subData'] };
return docService.loadMedicalInterests(postData).then(function(resp) {
me['SysMedicalInterests'] = resp['data'];
}, function(){});
};
loadMedicalInterests();
so array starts with a null reference and loads data from server. which changes array causes a second filter run. but it doesnt stop after that
Edit: here is plunkr http://plnkr.co/edit/OmHQ62VgiCXeVzKa5qjz?p=preview
Edit: related answer on so https://stackoverflow.com/a/21653981/1666060 but this still doesn't explain angular built in filters.
here is angularjs limitTo filter source code
https://github.com/angular/angular.js/blob/master/src/ng/filter/limitTo.js#L3
About what exactly causes it, I suspect is something to do with the fact that every time you run the filter a new array reference is created and returned. However, Angular's built-in filter filter does the same thing, so I'm not sure what is going wrong. It could be something to do with the fact that it's an array of arrays that is being returned.
The best I have come up with is a workaround/hack, to cache the array reference manually as an added property, which I've called $$splitListFilter on the array, and only change it if it fails a test on angular.equals with the correct results calculated in the filter:
app.filter('splitListFilter', function () {
return function (data, chunk) {
if(!data || data.length === 0){
return data;
}
var results = [];
for (var i = 0, j = data.length; i < j; i += chunk) {
results.push(data.slice(i, i + chunk));
}
if (!data.$$splitListFilter || !angular.equals(data.$$splitListFilter, results)) {
data.$$splitListFilter = results;
}
return data.$$splitListFilter;
};
});
You can see this working at http://plnkr.co/edit/vvVJcyDxsp8uoFOinX3V
The answer uses Angular 1.3.15
The JS fiddle works fine: http://jsfiddle.net/3tzapfhh/1/
Maybe you use the filter wrongly.
<body ng-app='app'>
<div ng-controller='ctrl'>
{{arr | splitListFilter:3}}
</div>
</body>

Angular nested ng-repeat filter items matching parent value

I am passing in 2 arrays to my view. I would like my nested loop to only display where it's parent_id value matches the parent.id. Eg.
arr1 = {"0":{"id":326,"parent_id":0,"title":"Mellow Mushroom voucher","full_name":"Patrick","message":"The voucher says $10 Voucher; some wording on the printout says, \"This voucher is valid for $20 Pizza\" but my purchase price or amount paid also says $20. Shouldn't that be $10","type":"Deals"}};
arr2 = {"0":{"id":327,"parent_id":326,"title":"Re: Mellow Mushroom voucher","full_name":"Patrick Williams","message":"Some message here","type":null};
...
<div data-ng-repeat = "parent in arr1">
<span>{{parent.title}}<span>
<div data-ng-repeat="child in arr2 | only-show-where-child.parent_id == parent.id">
<li>{{child.body}}</li>
</div>
</div>
Is this possible/best practice in angular of should I be filtering the object in node before passing it into angular? Thank you!
There are a couple of ways you could do it... You could create a function to return just the children:
$scope.getChildren = function(parent) {
var children = [];
for (var i = 0; i < arr2.length; i++) {
if (arr2[i].parent_id == parent.id) {
children.push(arr2[i]);
}
}
return children;
};
html:
<div ng-repeat="child in getChildren(parent)">
You could define a filter to do the same thing:
myApp.filter('children', function() {
return function(input, parent) {
var children = [];
for (var i = 0; i < input.length; i++) {
if (input[i].parent_id == parent.id) {
children.push(input[i]);
}
}
return children;
};
});
html:
<div ng-repeat="child in arr2|children:parent">
Both of those methods will execute every digest cycle though. If you have a large list of elements you would definitely want to improve performance. I think the best way would be to pre-process those results when you get them, adding a children array to each object in arr1 with only its children (here using array.filter instead of for loop and array.forEach):
arr1.forEach(function(parent) {
parent.children = arr2.filter(function(value) {
return value.parent_id === parent.id;
};
});
Then in the html you are already working with the parent so you can repeat over its children property:
<div ng-repeat="child in parent.children">
Instead of using filters, data-ng-if can achieve the same result.
<div data-ng-repeat="parent in arr1">
<span>{{parent.title}}<span>
<div data-ng-repeat="child in arr2" data-ng-if="child.parent_id == parent.id">
<li>{{child.body}}</li>
</div>
</div>
The solution depends on how often arrays are changed and how big arrays are.
The fist solution is to use filter. But in this case it would be called at least twice (to make sure that result is "stabilized" - selected same elements).
Other solution is to $watch by yourself original array and prepare "view" version of it injecting children there. Personally I would prefer the second as more explicit.
However if you can reuse "find-the0child" filter in other parts of your application you can go with first one - AngularJS will re-run filter only after original array modified.
If needed I can provide here an example of implementation of one of these options - add the comment to answer.

angular $index numbering with condition in ng-repeat & ui-sortable

I'm trying to achieve customized numbering while listing all items in an array.
All items in array are rendered using ng-repeat & ui.sortable.
Numbering must be done in such a way that, for an array item "statement", count should not be increased & displayed.
(Else I may be used $index instead of an external count.)
For any other array item, count should be increased & displayed.
The solution that got me the the closest result was the one where I passed $index into a filter function written in the controller.
like in HTML:
<li ng-repeat="question in questions">
<div class="no"> {{ filterIndex(question.question, $index) }} </div>
<div>{{question.question}}</div>
</li>
in controller:
var filterValue = 0;
$scope.filterIndex = function (value, count) {
if (count === 0) {
filterValue = 0;
}
if (value !== 'statementText') {
filterValue = filterValue + 1;
return filterValue;
}
else {
return '"';
}
};
Even it was working without any errors, the count returned from function is not get updated like we get with $index when we update the order using ui-sortable.
see it here: js-fiddle using filter function
means, once it rendered (4) in <[ (4) fourth question ]> will remain same even if we moved it to top or bottom by dragging.
I tried different ways and almost everything ended up on 'Maximum iteration limit exceeded.'.
Real scenario is a little bit complex as it contains nested ng-repeats and similar counting with digits and numbers alternatively in child loops.
Here is link to start fresh:
js-fiddle
You can inject this to your controller, it will listen for array changes, and update the indexes:
var update = function(){
var currentCount = 0;
var questions = $scope.questions;
for(var j = 0; j < questions.length; j++){
if(questions[j].question != null){
if(questions[j].question.indexOf("statement") < 0){
questions[j].calculatedIndex = ++currentCount;
}
}
}
};
$scope.$watchCollection('questions', function(){
update();
});
update();
probably needs a bit fine-tuning as I didn't concentrate on your question completely. In the ng-repeat, you now have access to calculatedIndex. It will be "NULL" for statement items, you can use ng-show for that.
just try to add extra option:
ng-init='question.index = $index+1'
http://jsfiddle.net/ny279bry/

Angular.js ng-repeat: opening/closing elements after x iterations

I have a simple ng-repeat going on:
<div class="row-fluid">
<div class="span4" ng-repeat="piece in clothes | filter:query">
<img ng-src="{{piece.mainImage[0]}}" class="thumbImg" />
{{piece.name}}
</div>
</div>
After 3 repeats, I'd like to stop the repeat, add in a closing <div> and open a new .row-fluid (to start a new line), then re-start the loop where I left off, inserting the tags each 3rd time.
The docs for Angular are really hard to traverse, making it difficult to work out how to do this.
You could create a filter for array partitioning.
(If you can use some library, you may be able to get something shorter & more efficient.)
app.filter('partition', function() {
var part = function(arr, size) {
if ( 0 === arr.length ) return [];
return [ arr.slice( 0, size ) ].concat( part( arr.slice( size ), size) );
};
return part;
});
You can use it like:
<div ng-repeat="rows in list | partition:3">
<div class="row" ng-repeat="item in rows">
<div class="span4">{{ item }}</div>
</div>
</div>
ngRepeat will keep re-evaluating the filter infinitely because it gets different values every time it's ran.
This terrible code seems to work in the general case. If you have identical arrays things could get hairy. We need to be able to compare arrays for equality, but that's a difficult problem. I took the easy/flimsy way out and just used stringify(). Could not get angular.equals() to work for me; I think it probably just works on shallow objects.
app.filter('partition', function($cacheFactory) {
var arrayCache = $cacheFactory('partition')
return function(arr, size) {
var parts = [], cachedParts,
jsonArr = JSON.stringify(arr);
for (var i=0; i < arr.length; i += size) {
parts.push(arr.slice(i, i + size));
}
cachedParts = arrayCache.get(jsonArr);
if (JSON.stringify(cachedParts) === JSON.stringify(parts)) {
return cachedParts;
}
arrayCache.put(jsonArr, parts);
return parts;
};
});

Resources