angular Infinite $digest Loop in ng-repeat - angularjs

I want to call function in ng-repeat attribute, here is my code
example plnkr
html
<body ng-controller="mainCtrl">
<div ng-repeat='item in getGroupedRange() track by item.id'>
<span>{{item.val}}</span>
<span>{{item.abs}}</span>
<span>{{item.rel}}</span>
<span>{{item.cum}}</span>
</div>
</body>
js
$scope.getGroupedRange = function() {
return [
{
val: 1,
abs: 1,
rel: 1,
cum: 1,
id: 123456
}
];
};
When I opened console I noticed the error
10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: function (c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}","newVal":9,"oldVal":8}],[{"msg":"fn: function (c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}","newVal":10,"oldVal":9}],[{"msg":"fn: function (c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}","newVal":11,"oldVal":10}],[{"msg":"fn: function (c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}","newVal":12,"oldVal":11}],[{"msg":"fn: function (c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}","newVal":13,"oldVal":12}]]
The main goal of my code is using function in ng-repeat for calculating data in each event loop

No, you can't use function in ngRepeat like this. The problem is that Angular uses strict comparison of objects in digest loop to determine if the value of the property changed since the last check. So what happens is that getGroupedRange returns new value (new array) each time it's called. Angular has no idea and considers this value as unstable and thus continues checking. But it aborts after 10 checks.
You need to construct necessary array and assign it to scope property, so it will not change during digest loop:
$scope.groupedRange = $scope.getGroupedRange();
then use it like in ngRepeat
<div ng-repeat='item in groupedRange track by item.id'>
<span>{{item.val}}</span>
<span>{{item.abs}}</span>
<span>{{item.rel}}</span>
<span>{{item.cum}}</span>
</div>

Angular will do calculation and automatic showing in template on data change for you. But by putting ng-repeat='item in getGroupedRange() you put this into endles cycle recalculation.
Try to avoid this by assigning the value of the items (that may be changed by $scope.getGroupedRange function in any way) in the list to some scope variable, say $scope.range, that will be iterated in ng-repeat.
in controller
$scope.getGroupedRange = function() {
$scope.range = [
{
val: 1,
abs: 1,
rel: 1,
cum: 1,
id: 123456
}
];
};
in template
<div ng-repeat='item in range track by item.id'>
<span>{{item.val}}</span>
<span>{{item.abs}}</span>
<span>{{item.rel}}</span>
<span>{{item.cum}}</span>
</div>

Found the solution:
$scope.prev = null;
$scope.getGroupedRange = function() {
var data = [{
val: 1,
abs: 1,
rel: 1,
cum: 1,
id: 123456
}];
if (angular.equals($scope.prev, data)) {
return $scope.prev;
}
$scope.prev = data;
return data;
};

Try doing something like this:
<body ng-controller="mainCtrl">
<div ng-init="grpRange = getGroupedRange()">
<div ng-repeat='item in grpRange track by item.id'>
<span>{{item.val}}</span>
<span>{{item.abs}}</span>
<span>{{item.rel}}</span>
<span>{{item.cum}}</span>
</div>
</div>
</body>

Related

Function in ng-bind , multiple call?

I couldnt fint the answer on stack or google...
Why function in ng-bind call many times ?
html:
<li ng-if="byProviders" ng-repeat="(key, value) in byProviderGames | groupBy: 'provider'">
<p ng-bind="providersNames(key)"></p>
</li>
controller:
$scope.providersNames = function providersNames(key) {
// providersObject's length is 8
var index = $scope.providersObject.findIndex(function(x){ return x.name == key });
// Call more then 1000 times
console.log($scope.gamesProviders[index]);
var title = $scope.providersObject[index].title;
return title;
}
From the ngBind source code we can see that this directive registers a ngBindWatchAction callback to change element.textContent whenever the ng-bind attribute changes using scope.$watch:
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
element.textContent = stringify(value);
});
The watchExpression that is used as a first argument of the scope.$watch method is called on every call to $digest() (at least 1 time) and returns the value that will be watched (but it may be executed multiple times to check if the model was not changed by by other watchers). This is mentioned in the docs:
Be prepared for multiple calls to your watchExpression because it will
execute multiple times in a single $digest cycle if a change is
detected.
In this example $scope.getName method will be called 11 times until its returned value become stable:
angular.module('bindExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.name = 'Arik';
$scope.count = 0;
$scope.getName = function() {
if ($scope.count < 10) { $scope.count++; }
console.log($scope.count);
return $scope.name + $scope.count;
};
}]);
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<body ng-app="bindExample">
<div ng-controller="ExampleController">
Hello <span ng-bind="getName()"></span>!
</div>
</body>
There are a couple things you can do to help with performance and how it affects the number of times the function is called.
First, you can add a track by $index statement to the end of the ng-repeat. It would end up looking like this.
ng-repeat="(key, value) in byProviderGames | groupBy: 'provider' track by $index"
Second, if just displaying data and no other interaction will take place you can unbind them from the scope. So it'll no longer dirty check for changes. It'll end up looking like this.
ng-repeat="(key, value) in ::byProviderGames | groupBy: 'provider'"
Superpower it with both.
ng-repeat="(key, value) in ::byProviderGames | groupBy: 'provider' track by $index"
You can even go as far as to unbind the actual value.
<p ng-bind="::providersNames(key)"></p>
I'm including a code pen, so you can see how many times the function is called. Nowhere as much as you indicated above. Hope it helps. CodePen
Unbind, properly known by (one-time-binding)[https://docs.angularjs.org/guide/expression]
One-time binding
An expression that starts with :: is considered a one-time expression. One-time expressions will stop recalculating once they are stable, which happens after the first digest if the expression result is a non-undefined value (see value stabilization algorithm below).
function exampleController($scope) {
$scope.exampleArray = [1, 2, 3, 4];
var exampleLookupArray = [
{ name: "Schnauzer" },
{ name: "Dachshund" },
{ name: "German Shepard" },
{ name: "Doberman Pinscher" }
];
$scope.breedLookUp = function(key) {
console.count();
return exampleLookupArray[key-1].name;
};
}
angular
.module("example", [])
.controller("exampleController", exampleController);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div class="container-fluid" ng-app="example">
<div class="container" ng-controller="exampleController">
<div ng-repeat="ex in ::exampleArray track by $index">
<span ng-bind="::breedLookUp(ex)"></span>
</div>
</div>
</div>

$watch for value changes in array of objects which is updated by ng-click

I am new to Angular and have some difficulties with my first application.
I have array of objects retrieved from database: $scope.result
By clicking on each ng-repeat element through ng-click I push that element to $scope.selectedTags array and use another ng-repeat on the view. This second ng-repeat has two input fields. I want to $watch for changes on every element of $scope.selectedTags and if there are changes to make some calculations with the input values. I am using angular.forEach to $watch on every element in array but it doesn't work correctly if there are more than one element in the array.
HTML:
<div ng-app="application" ng-controller="controller">
<ul>
<li ng-repeat="r in result" ng-click="addToSelectedTags($index)">{{r.name}}</li>
</ul>
<ul>
<li ng-repeat="t in selectedTags">{{t.name}}<input type="text" ng-model="t.n1" /> <input type="text" ng-model="t.n2" /></li>
</ul>
</div>
Javascript code:
<script>
var app = angular.module("application", []);
app.controller("controller", function($scope){
$scope.result = [
{name: "first", n1: 2, n2: 6},
{name: "second", n1: 7, n2: 1},
{name: "third", n1: 4, n2: 9},
{name: "fourth", n1: 5, n2: 2}
];
$scope.selectedTags = [];
$scope.addToSelectedTags = function(index) {
var selectedtag = $scope.result[index];
$scope.selectedTags.push(selectedtag);
angular.forEach($scope.selectedTags, function(value, key) {
$scope.$watchGroup(['selectedTags['+key+'].n1', 'selectedTags['+key+'].n2'], function(newval, oldval){
if(newval[1] !== oldval[1]) {
$scope.selectedTags[key].n1 = newval[1]/oldval[1]*$scope.selectedTags[key].n1;
oldval[1] = newval[1];
}
});
});
};
});
</script>
If there are two elements in $scope.selectedTags and change the value of the first element the $watch runs twice and make the wrong calculation. If there are three elements in $scope.selectedTags it runs three times and so on.
I tried to put the whole angular.forEach outside the addToSelectedTags() function but in that way it doesn't $watch for changes at all. Could you tell me how can I handle with this problem?
Plunker
Alright I got something working : your updated plunkr
Basically what caused the bug behaviour you had was that you created a new watch everytime you pushed a new element to selectedTags. By having multiple watches on an element, it calls your function for every watch currently watching over it. You only need one watch per element so I replaced the group watch by individual watches on your objects :
$scope.addToSelectedTags = function(index) {
var selectedtag = $scope.result[index];
$scope.selectedTags.push(selectedtag);
$scope.$watchCollection('selectedTags[' + ($scope.selectedTags.length - 1)+ ']', function(newval, oldval){
//Not sure about the calculation you want, just change it to whatever behaviour you need
newval.n1 = newval.n2/oldval.n2*newval.n1
});
};
I am not sure about the operation you want though you may have to change it just under my comment. Best of luck!

AngularJs: loop through number

I'm looping a number (from 0 to 7) to get the index of the next day.
Here bellow is a fiddle working.
The problem is the first day is not "Monday", but Friday. So, the number is not 0 but 4...
I do not understand where is the problem.
Please help
(function(){
var app = angular.module('myApp', [ ]);
app.controller('CalenderController', function(){
this.firstDay = -1;
this.getDayName = function(){
this.firstDay++;
if(this.firstDay == 7){
this.firstDay = 0;
}
return dayNames[this.firstDay];
};
this.dayLength = function(){
return new Array(13);
}
});
//Variables
var dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js"></script>
<div class="container" ng-app="myApp" ng-controller="CalenderController as calender">
<div ng-repeat="item in calender.dayLength() track by $index">
{{calender.getDayName()}}
</div>
</div>
It is a very bad idea to leave side-effects in functions that are being watched by Angular. Any function that is called from within an expression {{something()}} will be evaluated on every digest cycle, and so, these functions must be idempotent.
The getDayName function is not idempotent, because it changes this.firstDay.
Not only that, but it also returns a different value every time it's called, and so it causes the digest cycle to re-run (until it's aborted by Angular after 10 iterations).
Instead, use the $index directly to access the dayName array:
<div ng-repeat="item in calendar.dayLength()">
{{calendar.dayNames[$index % 7]}}
</div>
and expose dayNames as a VM with this.dayNames.
EDIT: On second thought, it's better to expose this as a function, so that you could do mod 7 there:
$scope.getDayName = function(dayIndex){
return dayNames[dayIndex % 7];
}
and in the View:
{{calendar.getDayName($index)}}
EDIT 2: If you don't need to have a flat DOM hierarchy of <div>s for all the days over 2 weeks, you could even do this much simpler:
<div ng-repeat="week in [0, 1]">
<div ng-repeat="day in dayNames">
{{day}}
</div>
</div>

Something like "with variable" in AngularJS?

Sample controller data:
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
Is there something in angular to make the following code work like "with variable"?
<ul>
<li data-ng-repeat="items">{{id}} {{name}}</li>
</ul>
Instead of:
<ul>
<li data-ng-repeat="i in items">{{i.id}} {{i.name}}</li>
</ul>
Please feel free to make a more understandable title/question.
Referring to Angular ngRepeat document, currently only followings expressions are supported.
variable in expression
(key, value) in expression
variable in expression track by tracking_expression
variable in expression as alias_expression
This means, you can't simply use ng-repeat="items" to iterate the collection.
BTW, ng-repeat will create a separate scope for each element and bind variable or (key, value) to the newly created scope. So "with variable" you refer to is not Angular built-in. You need to create a customized directive for this functionality.
My preferred answer would be "don't do this" but failing that, and because it's interesting, here's a proof of concept, assisted by this question and mostly adapted from this blog post:
app.directive('myRepeat', function(){
return {
transclude : 'element',
compile : function(element, attrs, linker){
return function($scope, $element, $attr){
var collectionExpr = attrs.myRepeat;
var parent = $element.parent();
var elements = [];
// $watchCollection is called everytime the collection is modified
$scope.$watchCollection(collectionExpr, function(collection) {
var i, block, childScope;
// check if elements have already been rendered
if(elements.length > 0){
// if so remove them from DOM, and destroy their scope
for (i = 0; i < elements.length; i++) {
elements[i].el.remove();
elements[i].scope.$destroy();
};
elements = [];
}
for (i = 0; i < collection.length; i++) {
// create a new scope for every element in the collection.
childScope = $scope.$new();
// ***
// This is the bit that makes it behave like a `with`
// statement -- we assign the item's attributes to the
// child scope one by one, rather than simply adding
// the item itself.
angular.forEach(collection[i], function(v, k) {
childScope[k] = v;
});
// ***
linker(childScope, function(clone){
// clone the transcluded element, passing in the new scope.
parent.append(clone); // add to DOM
block = {};
block.el = clone;
block.scope = childScope;
elements.push(block);
});
};
});
}
}
}
});
And then this will do what you want:
app.controller("myController", function($scope, $http) {
$scope.items = [
{a: 123, b: 234},
{a: 321, b: 432}
];
});
With the HTML structure you want:
<div ng-controller="myController">
<ul>
<li my-repeat="items">
{{ a }} {{ b }}
</li>
</ul>
</div>
Notice that given the attributes are copied into the child scopes, rather than referenced, if changes are made to the view, they won't affect the model (ie. the parent items list), severely limiting the usefulness of this directive. You could hack around this with an extra scope.$watch but it'd almost certainly be less fuss to use ng-repeat as it's normally used.
I can't see why other users are telling you that what you want has to be done via a new directive. This is a working snippet.
angular.module("Snippet",[]).controller("List",["$scope",function($scope){
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="Snippet" ng-controller="List as list">
<ul>
<!-- Iterating the array -->
<li ng-repeat="item in items">
<!-- Iterating each object of the array -->
<span ng-repeat="(key,value) in item">{{value}} </span>
</li>
</ul>
</body>
Simply, you need to iterate the elements of the array via ng-repeat, then you can do what you want with the object item retrieved. If you want to show its values, for example, as it seems for your question, then a new ng-repeat gets the job done.

angularjs infinite digest errors when looping through a list

Why does the following give the error:
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Code
<div ng-app>
<h2>Todo</h2>
<div ng-controller="TodoCtrl">
<span ng-bind="getText()"></span>
</div>
</div>
function TodoCtrl($scope) {
$scope.todos = [
{text:'learn angular', done:true},
{text:'build an angular app', done:false}];
$scope.getText = function() {
var names = $scope.todos.map(function(t) {
return t.text;
});
return names;
}
};
The code block is supposed to grab all todos and then render their names in a list using ng-bind. It works, but tons of digest iteration errors show up in console.
jsfiddle
It is really a bad practice to use a function evaluation in ng-bind, reason for this infinite digest cycle is because your digest cycle never gets settled. Everytime digest cycle happens ng-bind expression also runs and since the return value from ng-bind expression is always different (different object reference produced by array.map) it has to rerun the digest cycle again and it goes on until reached the max limit set, i.e 10.
In your specific case you could just set the names as a scope property and ng-bind="name".
$scope.names = $scope.todos.map(function(t) {
return t.text;
}).join();
As a general rule you can make sure you update the property name only when needed from your controller, example when an event occurs like adding a todo, removing a todo etc.. A typical scenario in this answer. You could also use interpolation instead of ng-bind and use function expression. {{}}. ie:
$scope.getText = function() {
return $scope.todos.map(function(t) {
return t.text;
}).join();
}
and
<span>{{getText()}}</span> <!--or even <span ng-bind="getText()"></span>-->
Fiddle
I feel like you have over complicated this i have updated the fiddle with a working solution http://jsfiddle.net/U3pVM/12417/.
<div ng-app>
<h2>Todo</h2>
<div ng-controller="TodoCtrl">
<div ng-repeat="todo in todos">
<span >{{ todo.text}}</span>
</div>
</div>
</div>
function TodoCtrl($scope) {
$scope.todos = [
{text:'learn angular', done:true},
{text:'build an angular app', done:false}];
};

Resources