Function in ng-bind , multiple call? - angularjs

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>

Related

Share $scope between DOM elements with same ng-controller

I'm stuck with something (I've been reading similar questions but none of them get me to the solution).
I have 2 DOM elements (let's say 2 div's) with different id's but the same ng-controller (this is for basic example, in my real app I have 2 differente pages but works the same).
<div id="layer1" ng-controller="appCtrl">
<select ng-model="selectedType" ng-options="type.label for type in ptype track by type.value" ng-change="changeType(selectedType.value)"></select>
</div>
<div id="layer2" ng-controller="appCtrl">
<select ng-model="selectedType" ng-options="type.label for type in ptype track by type.value" ng-change="changeType(selectedType.value)"></select>
</div>
And in JS
var myAppModule = angular.module('myApp', [])
.factory('selectedType', function(){
return{}
})
.controller('appCtrl', ['$scope',function($scope){
$scope.ptype = [
{
value: 1,
label:'Kg'
},
{
value: 2,
label:'Pza'
}];
selectedType = $scope.ptype[0];
$scope.changeType = function(value){
if(value==1){selectedType = $scope.ptype[0];}
else{selectedType=$scope.ptype[1];}
};
}])
As you can see I have the options for the SELECT and the ng-model, what I need is when change the selected value in any SELECT (doesn't matter which DIV) the other gets updated too.
Here a Plunker with code SEE HERE.
Thanks!
Using a Shared service like your attempt:
you are in the good way but you just forgot that :
1 - you use the scope in ng-model,
2 - to have 2 way data binding for factory,
bind the factory itself to a scope,
and use an item inside the factory not the factory itself (eg: data).
code:
.factory('selectedType', function(){
return {
data: {} // <---- We use 'data' here for example
}
})
and in the controller now:
.controller('appCtrl', ['$scope', 'selectedType',function($scope, selectedType) {
$scope.selectedType = selectedType; // <-- Not the selectedType.data (Important)
/* but the factory object */
$scope.ptype = [
{
value: 1,
label:'Kg'
},
{
value: 2,
label:'Pza'
}];
$scope.selectedType.data = $scope.ptype[0];
})
Now we dont need the ng-change at all:
<div id="layer1" ng-controller="appCtrl">
<select ng-model="selectedType" ng-options="type.label for type in ptype track by type.value"></select>
</div>
<div id="layer2" ng-controller="appCtrl">
<select ng-model="selectedType" ng-options="type.label for type in ptype track by type.value"></select>
</div>
plunker: https://plnkr.co/edit/5qg45F?p=preview
NB: Instead of using shared service, you can also use $rootScope or $scope.$parent for that.

angular Infinite $digest Loop in ng-repeat

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>

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>

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}];
};

Math functions in AngularJS bindings

Is there a way to use math functions in AngularJS bindings?
e.g.
<p>The percentage is {{Math.round(100*count/total)}}%</p>
This fiddle shows the problem
http://jsfiddle.net/ricick/jtA99/1/
You have to inject Math into your scope, if you need to use it as
$scope know nothing about Math.
Simplest way, you can do
$scope.Math = window.Math;
in your controller.
Angular way to do this correctly would be create a Math service, I guess.
While the accepted answer is right that you can inject Math to use it in angular, for this particular problem, the more conventional/angular way is the number filter:
<p>The percentage is {{(100*count/total)| number:0}}%</p>
You can read more about the number filter here: http://docs.angularjs.org/api/ng/filter/number
I think the best way to do it is by creating a filter, like this:
myModule.filter('ceil', function() {
return function(input) {
return Math.ceil(input);
};
});
then the markup looks like this:
<p>The percentage is {{ (100*count/total) | ceil }}%</p>
Updated fiddle: http://jsfiddle.net/BB4T4/
This is a hairy one to answer, because you didn't give the full context of what you're doing. The accepted answer will work, but in some cases will cause poor performance. That, and it's going to be harder to test.
If you're doing this as part of a static form, fine. The accepted answer will work, even if it isn't easy to test, and it's hinky.
If you want to be "Angular" about this:
You'll want to keep any "business logic" (i.e. logic that alters data to be displayed) out of your views. This is so you can unit test your logic, and so you don't end up tightly coupling your controller and your view. Theoretically, you should be able to point your controller at another view and use the same values from the scopes. (if that makes sense).
You'll also want to consider that any function calls inside of a binding (such as {{}} or ng-bind or ng-bind-html) will have to be evaluated on every digest, because angular has no way of knowing if the value has changed or not like it would with a property on the scope.
The "angular" way to do this would be to cache the value in a property on the scope on change using an ng-change event or even a $watch.
For example with a static form:
angular.controller('MainCtrl', function($scope, $window) {
$scope.count = 0;
$scope.total = 1;
$scope.updatePercentage = function () {
$scope.percentage = $window.Math.round((100 * $scope.count) / $scope.total);
};
});
<form name="calcForm">
<label>Count <input name="count" ng-model="count"
ng-change="updatePercentage()"
type="number" min="0" required/></label><br/>
<label>Total <input name="total" ng-model="total"
ng-change="updatePercentage()"
type="number" min="1" required/></label><br/>
<hr/>
Percentage: {{percentage}}
</form>
And now you can test it!
describe('Testing percentage controller', function() {
var $scope = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope
});
}));
it('should calculate percentages properly', function() {
$scope.count = 1;
$scope.total = 1;
$scope.updatePercentage();
expect($scope.percentage).toEqual(100);
$scope.count = 1;
$scope.total = 2;
$scope.updatePercentage();
expect($scope.percentage).toEqual(50);
$scope.count = 497;
$scope.total = 10000;
$scope.updatePercentage();
expect($scope.percentage).toEqual(5); //4.97% rounded up.
$scope.count = 231;
$scope.total = 10000;
$scope.updatePercentage();
expect($scope.percentage).toEqual(2); //2.31% rounded down.
});
});
Why not wrap the whole math obj in a filter?
var app = angular.module('fMathFilters',[]);
function math() {
return function(input,arg) {
if(input) {
return Math[arg](input);
}
return 0;
}
}
return app.filter('math',[math]);
and to use:
{{ number_var | math:'ceil'}}
Better option is to use :
{{(100*score/questionCounter) || 0 | number:0}}
It sets default value of equation to 0 in the case when values are not initialized.
If you're looking to do a simple round in Angular you can easily set the filter inside your expression. For example:
{{ val | number:0 }}
See this CodePen example & for other number filter options.
Angular Docs on using Number Filters
The easiest way to do simple math with Angular is directly in the HTML markup for individual bindings as needed, assuming you don't need to do mass calculations on your page. Here's an example:
{{(data.input/data.input2)| number}}
In this case you just do the math in the () and then use a filter | to get a number answer. Here's more info on formatting Angular numbers as text:
https://docs.angularjs.org/api/ng/filter
Either bind the global Math object onto the scope (remember to use $window not window)
$scope.abs = $window.Math.abs;
Use the binding in your HTML:
<p>Distance from zero: {{abs(distance)}}</p>
Or create a filter for the specific Math function you're after:
module.filter('abs', ['$window', function($window) {
return function(n) {
return $window.Math.abs($window.parseInt(n));
};
});
Use the filter in your HTML:
<p>Distance from zero: {{distance | abs}}</p>
That doesn't look like a very Angular way of doing it. I'm not entirely sure why it doesn't work, but you'd probably need to access the scope in order to use a function like that.
My suggestion would be to create a filter. That's the Angular way.
myModule.filter('ceil', function() {
return function(input) {
return Math.ceil(input);
};
});
Then in your HTML do this:
<p>The percentage is {{ (100*count/total) | ceil }}%</p>
The number filter formats the number with thousands separators, so it's not strictly a math function.
Moreover, its decimal 'limiter' doesn't ceil any chopped decimals (as some other answers would lead you to believe), but rather rounding them.
So for any math function you want, you can inject it (easier to mock than injecting the whole Math object) like so:
myModule.filter('ceil', function () {
return Math.ceil;
});
No need to wrap it in another function either.
This is more or less a summary of three answers (by Sara Inés Calderón, klaxon and Gothburz), but as they all added something important, I consider it worth joining the solutions and adding some more explanation.
Considering your example, you can do calculations in your template using:
{{ 100 * (count/total) }}
However, this may result in a whole lot of decimal places, so using filters is a good way to go:
{{ 100 * (count/total) | number }}
By default, the number filter will leave up to three fractional digits, this is where the fractionSize argument comes in quite handy
({{ 100 * (count/total) | number:fractionSize }}), which in your case would be:
{{ 100 * (count/total) | number:0 }}
This will also round the result already:
angular.module('numberFilterExample', [])
.controller('ExampleController', ['$scope',
function($scope) {
$scope.val = 1234.56789;
}
]);
<!doctype html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</head>
<body ng-app="numberFilterExample">
<table ng-controller="ExampleController">
<tr>
<td>No formatting:</td>
<td>
<span>{{ val }}</span>
</td>
</tr>
<tr>
<td>3 Decimal places:</td>
<td>
<span>{{ val | number }}</span> (default)
</td>
</tr>
<tr>
<td>2 Decimal places:</td>
<td><span>{{ val | number:2 }}</span></td>
</tr>
<tr>
<td>No fractions: </td>
<td><span>{{ val | number:0 }}</span> (rounded)</td>
</tr>
</table>
</body>
</html>
Last thing to mention, if you rely on an external data source, it probably is good practise to provide a proper fallback value (otherwise you may see NaN or nothing on your site):
{{ (100 * (count/total) | number:0) || 0 }}
Sidenote: Depending on your specifications, you may even be able to be more precise with your fallbacks/define fallbacks on lower levels already (e.g. {{(100 * (count || 10)/ (total || 100) | number:2)}}). Though, this may not not always make sense..
Angular Typescript example using a pipe.
math.pipe.ts
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'math',
})
export class MathPipe implements PipeTransform {
transform(value: number, args: any = null):any {
if(value) {
return Math[args](value);
}
return 0;
}
}
Add to #NgModule declarations
#NgModule({
declarations: [
MathPipe,
then use in your template like so:
{{(100*count/total) | math:'round'}}

Resources