Lot of firebase queries cause "10 $digest() iterations reached" error - angularjs

For example, I want to make a list which looks like:
Apple - Steve Jobs
Microsoft - Bill Gates
Tesla - Elon Musk
And I'm going to do this, using two Firebase apps and AngularJS.
First FB app looks like:
companies
|_Apple: 0
|_Microsoft: 0
|_Tesla: 0
Second FB app:
companies-ceo
|_Apple: "Steve Jobs"
|_Microsoft: "Bill Gates"
|_Tesla: "Elon Musk"
So I have an AngularJs app:
angular.module('App', ["firebase"])
.controller('AppCtrl', ['$scope', '$firebaseObject',
function($scope,$firebaseObject){
var userRef = new Firebase('https://companies.firebaseio.com/');
var userObj = $firebaseObject(userRef);
userObj.$bindTo($scope, "userData");
$scope.getCEO = function(companyName){
var indexRef = new Firebase('https://companies-ceo.firebaseio.com/'+companyName);
var indexObj = $firebaseObject(indexRef);
indexObj.$loaded(function(){
return indexObj.$value;
})
};
}])
And an HTML code:
<div ng-controller="AppCtrl">
<div ng-repeat="(companyName,tmp) in userData">
<p>{{companyName}} - {{getCEO(companyName)}}</p>
</div>
</div>
When I don't use getCEO() function, like:
<div ng-controller="AppCtrl">
<div ng-repeat="(companyName,tmp) in userData">
<p>{{companyName}}</p>
</div>
</div>
everything is alright, but with getCEO() function is causes an error:
Error: 10 $digest() iterations reached. Aborting!
How can I fix it?

Because you called getCEO(companyName) in {{}} directive that are getting evaluated of each digest cycle which is causing digest cycle to reach its limitation and throwing an error. Instead of doing that I'd suggest you to call getCEO(companyName) method on div rendering time of ng-repeat nothing but I'm gonna use ng-init here
Markup
<div ng-repeat="(companyName,tmp) in userData" ng-init="ceo=getCEO(companyName)">
<p>{{companyName}} - {{ceo}}</p>
</div>
OR
Another good example would be using :: bindonce directive will run the specified method runs on only once. By using this directive getCEO(companyName) method will call only once.
Markup
<div ng-repeat="(companyName,tmp) in userData">
<p>{{companyName}} - {{::getCEO(companyName)}}</p>
</div>
NOTE:- Bindonce :: directive will need Angular 1.3+ version

Related

AngularJS. Prevent updating ALL data in $scope

I am novice in Angular and I have a question.
I noticed that angular updates all scope data on view (am I right?), even if it has been changed only one variable (that renders on view). Is it normal ? What if I have large data on view and I want to update it only when this is data being changed.
Code for example (every time when scope.word is being modified function func is executing):
<div ng-app="myApp" ng-controller="myCtrl">
Word: <input ng-model="word">
{{func()}}
</div>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.word = "John Doe";
$scope.func = function(){
alert("Who dared to disturb me !? >(");
};
});
</script>
Is it normal? - You bet it's normal, this is the whole idea.
What you're doing is not a good practice at all. However, because when you bind a function as an expression in the view, Angular doesn't "know" when it should update the expression in the view, so it updates it on every digest cycle that happens a lot! Almost every time the user interacts with the view (Click, scroll) or if anything is changed on the controller side, so you might find yourself ending up with this error.
You should bind properties to the view, not functions. Example:
angular.module('app',[]).controller('ctrl', function($scope) {
$scope.welcomeMessage = "Hi, welcome to AngularJS!";
$scope.updateMessage = function(message) {
$scope.welcomeMessage = message;
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<strong>{{ welcomeMessage }}</strong>
<hr>
<input type="text" ng-model="msg">
<button ng-click="updateMessage(msg)">Update Message</button>
</div>
Note that if you know that you need to bind a property in the view only once, then you can use one time binding:
<strong>{{ ::welcomeMessage }}</strong>
Or
<strong ng-bind="::welcomeMessage"></strong>
By adding :: to the expression you prevent angular from tracking this expression after it is bound to the view the first time, and will not update it again, even if it was changed on the controller. Which is good for the performances of your app and can dramatically improve them.
Here is a working example of one-time binding: https://jsfiddle.net/hu9zcbwh/2/ (I couldn't create stack-snippet because it doesn't have angular 1.3 where this feature was first introduced)
I'm editing this with #MaximShoustin comment, that should help make this more clear and summarizes better the differences between the normal binding and one time binding:
ng-bind or {{}} generates one watcher and it will be fired after each digest cycle. On the other hand, :: expression creates watcher and cancels it once the value is not undefined
Sorry, not a native English speaker :(

nested sortable angular repeat not populating with $http

I'm an angular newby, and I'm trying to produce a sortable array within another sortable array using ng-sortable. When I get the data directly from a javascript file it works fine, but when I use $http to retrieve the same data, only the non-repeating data displays - I don't get any error, but the repeaters don't display at all.
Controller with test data (which works):
angular.module('demoApp').controller('TestController', function ($scope, TestService, TestDataFactory) {
$scope.testData = TestService.getTestData(TestDataFactory.Data);
});
Controller with $http:
angular.module('demoApp').controller('TestController', function ($scope, $http, TestService) {
$http.get('test/Index')
.success(function (data) {
$scope.testData = TestService.getTestData(data);
})
.error(function (data) {
alert("Error getting test data");
});
});
View:
<h1 class="dataname">{{data.name}}</h1>
<div ng-model="testData" id="test" ng-controller="TestController">
<div as-sortable="sectionSortOptions" ng-model="testData.sections">
<div class="section" ng-repeat="section in testData.sections" as-sortable-item ng-include="'test/Section'">
</div>
</div>
</div>
Test/Index returns a string of the same format as TestDataFactory.Data, and the object returned by TestService.getTestData() (which is just a data formatter) in each case is identical.
Am I missing something?
Edit:
Seems my problem is to do with the fact that my ng-include has another ng-sortable within it - here's a flattened view of the whole thing:
<h1 class="dataName">{{testData.name}}</h1>
<div as-sortable="sectionSortOptions" ng-model="testData.sections">
<div class="section" ng-repeat="section in testData.sections" as-sortable-item>
<span class="section-sortOrder" as-sortable-item-handle>Section {{section.sortOrder}}</span>
<div as-sortable="itemSortOptions" ng-model="section.items">
<div class="item" ng-repeat="item in section.items" as-sortable-item>
<div as-sortable-item-handle>
<span class="itemName">{{item.name}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
If I comment out the the line:
<div as-sortable="sectionSortOptions" ng-model="documentPack.sections">
And the associated as-sortable-item-handle (plus the closing div tag) then I get the sortable items in the sections (but not, obviously, the section-level sortability).
Plunker here: http://plnkr.co/edit/CNLVmlGjPvkcFKRo7SjN?p=preview - uses $timeout to simulate $http ($timeout caused the same problem) but now working as it uses latest ng-sortable (see answer).
I think this is your problem:
.success(function (data) {
$scope.testData = TestService.getTestData(data);
})
It should be
.success(function(data){
$scope.testData = data;
})
The success function is called when the promise is resolved, so if you do
$scope.testData = TestService.getTestData(data);
You are probably doing another remote call just as the first one finishes, and your page will never load anything.
Also, I suggest you read the documentation for ngResource as it will probably be useful in whatever you're doing.
It's not a good practice to use $http calls in your controllers, as you're coupling them with your remote calls, server addresses, and lots of other stuff that should be in another configuration files.
OK, seems the problem was with the version of ng-sortable I had - version number was labeled 1.2.2 (same as current version), but it didn't have the latest fixes (since 15th June - so, all of 15 days ago). Not sure how unlucky I am to have hit the (probably tiny) window when this problem was there, but I'll post this as answer just in case someone else hits similar behaviour with an early-June-2015 version.

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

Angular.js missing $scope update on page load

I've got a bit of an interesting situation where I have a weather app, and on loading the app, I want to get the users last position, and the last forecast they saw, and display that, while the app goes and gets the updated forecast.
In my controller, I construct the forecast view like this
function constructWeather(forecast){
if(!forecast.data.list){
$scope.loading='weather-error';
return;
}
var today = forecast.data.list[0];
console.log($scope.date);
var date = $scope.date;
$scope.print_date = $filter('date')(date,'EEEE, MMMM, d');
$scope.print_time = $filter('date')(date,'h:mm a');
$scope.weather = formatWeather(today,date,true);
$scope.city = $scope.position.name || forecast.data.city.name;
$scope.forecast = (function(){
var day_list=[];
for(var d=1; d<6; d++){
var day_weather = formatForecast(forecast.data.list[d],date,false);
day_list.push(day_weather);
}
return day_list;
})();
$scope.loading = 'weather-loaded';
}
This works fine when getting a new weather forecast, but when I am trying to reload the old forecast, all the $scope.print_date, $scope.weather, etc. etc. work fine, but ONLY the $scope.forecast the one executed within the anonymous function, does not display.
I've output to the console, and I see the day_weather is being built, but angular isn't updating.
In the template, it is loaded via
<div class="day" ng-repeat="day in forecast">
<div class="date">{{ day.print_date }}</div>
<div class="long-day">{{ day.long_day }}</div>
<div class="img {{weather.conditions | lowercase}}"></div>
<div class="temps">
<span class="degrees">{{ day.weather.temps.max }}</span>
<span class="min degrees">{{ day.weather.temps.min }}</span>
</div>
</div>
and of course, no divs are added to the page.
I've tried to run a $scope.$apply but it returns an error that I'm already in a digest cycle.
Is there another way to tell Angular that the $scope.forecast has new data?
Using closure like you did, works like that:
function declaration: (function() {...})
function execution: ()
So to the $scope.forcast the result of 2. is assigned. It is a completely static object.
If you want to execute this function many times, declare it this way:
$scope.forecast = function(){ ... };
and in you mark-up use it like that:
ng-repeat="day in forecast()"
PLNKR that shows it in action!
UPDATE
$scope.forecast = (function(){...})() can be replaced with:
$scope.getForecast = function () {...};
$scope.forecast = $scope.getForecast();
Then in your mark-up you can do something like that:
ng-repeat="day in (forecast=getForecast())"
For sure, it cannot be anything worse than what you had previously. If there is still some delay due to -- for example -- some asynchronous call, you should manually synchronize properties you want to be updated together.

underscore _.range() not work at AngularJS ng-repeat

If I want only 20 iteration, how can I repeat my block?
It isnt work:
<div ng-repeat="item in _.range(20)"></div>
UnderscoreJS included in the page
If you want to use undersore's functions in your template you will have to expose it on a scope. If you want to have it available in all templates one way of doing so would be:
var app = angular.module('angularjs-starter', []);
app.run(function($rootScope){
$rootScope._ = _;
});
Then you could use it in a template as you've tried:
<div ng-repeat="item in _.range(20)">{{item}}</div>
Here is a working plunk: http://plnkr.co/edit/1Va4EikvRyFiQvhb2HYV?p=preview
While the above works it shouldn't be used. Model should be initialized in a controller. Otherwise AngularJS will execute _range on each $digest cycle to generate a new array.

Resources