angular: ng-class in ng-repeater , duplicate call - javascript

first of all sorry for my English.
my problem is:
i've this simple code:
<li ng:repeat="item in menu.items" ng:class="getMenuItemClass(item)">
<a ng:href="#{{item.url}}">{{item.label}}</a>
</li>
and this my getMenuItemClass:
scope.getMenuItemClass = function(item) {
console.log(item)
var hashPath = $location.hashPath || '/';
if (hashPath === item.url) {
return 'selected';
}
return '';
};
this is the example ready,
i don't know why, but in my real application it's triplicate! :0
can someone explain to me if i'm making a mistake?
http://jsfiddle.net/h7yKr/44/
I've update the jsfddle to the last version of angular, and now it's quadruplicate!
watch it
http://jsfiddle.net/h7yKr/46/
edit for clarication:
the problem is that getMenuItemClass()
is called a lot more times more then required,
try to open the jsfiddle and open the browser console and watch the console.log!

Angular can evaluate an expression multiple times during a digest cycle and hence execution of function happens again and again.
As explained in the documentation here
Angular enters the $digest loop. The loop is made up of two smaller
loops which process $evalAsync queue and the $watch list. The $digest
loop keeps iterating until the model stabilizes, which means that the
$evalAsync queue is empty and the $watch list does not detect any
changes.
Since you $watch expression is a function it can get called multiple time.
Alternatively look at $routeChangeStart event in $route service.
You can subscribe to this event.
Create a variable on your scope and do the ng-class binding to that variable

Related

Angular 1.x - What's going on with the order of $scope?

I have a controller where I need to load content using ajax. While it's loading, I'd like a spinner to appear in the interim. The code looks something like the below:
<i class="fa fa-2x fa-spin fa-spinner" ng-show="isLoadingContent"></i>
And the corresponding js:
$scope.isLoadingContent = true;
$q.all(promises).then(function (values) {
$scope.isLoadingContent = false;
// more code - display returned data
However, the UI the spinner does not appear where/when I expect it to appear when I step through the code.
$scope.isLoadingContent = true;
debugger; // the spinner does not appear on the UI
$q.all(promises).then(function (values) {
debugger; // the spinner finally does appear in the UI at this point
$scope.isLoadingContent = false;
// more code - display returned data
I have tried stepping through the code but came up short as to what's going on --
and I am sure I am misunderstanding the sequence of events happening in the Event Loop and where the angular-cycle plays it's role in all of this.
Is someone able to provide an explanation as to why the spinner is set to appear within the promise's method rather than where I set $scope.isLoadingContent? Is it not actually getting set but rather getting queue'd up in the event-loop's message-queue?
------------ EDIT ------------
I believe I came across an explanation as to what's going on. Thanks in large part to, #jcford and #istrupin.
So a little tidbit missing in the original post, the event firing the promise calls and the spinner update was actually based around a $scope.$on("some-name", function(){...}) event - effectively a click-event that is triggered outside of my current controller's scope. I believe this means the $digest cycle doesn't work as it typically does because of where the event-origination is fired off. So any update in the $on function doesn't call $apply/$digest like it normally does, meaning I have to specifically make that $digest call.
Oddly enough, I realize now that within the $q.all(), it must call $apply since, when debugging, I saw the DOM changes that I had expected. Fwiw.
tl;dr - call $digest.
A combination of both answers will do the trick here. Use
$scope.$evalAsync()
This will combine scope apply with timeout in a nice way. The code within the $evalAsync will either be included in the current digest OR wait until the current digest is over and start a new digest with your changes.
i.e.
$q.all(promises).then(function (values) {
$scope.$evalAsync($scope.isLoadingContent = false);
});
Try adding $scope.$apply() after assigning $scope.isLoadingContent = true to force the digest. There might be something in the rest of your code keeping it from applying immediately.
As pointed out in a number of comments, this is absolutely a hack and is not the best way to go about solving the issue. That said, if this does work, you at least know that your binding is set up correctly, which will allow you to debug further. Since you mentioned it did, the next step would then be to see what's screwing up the normal digest cycle -- for example triggering outside of angular, as suggested by user JC Ford.
I usually use isContentLoaded (as oposite to isLoading). I leave it undefined at first so ng-show="!isContentLoaded" is guaranteed to show up at first template iteration.
When all is loaded i set isContentLoaded to true.
To debug your template you need to use $timeout
$timeout(function () { debugger; })
That will stop the code execution right after first digest cycle with all the $scope variable values reflected in the DOM.

Does AngularJS always know when something has changed, it's time to update?

I am reading AngularJS in Action by Lukas Ruebbelke to clear the concept of dirty checking as to how AngularJS works at a molecular level.
The author puts forward,
It is during the digest cycle that all watch expressions for a scope object
are evaluated. When a watch expression detects that a $scope property has
changed, then a listener function is fired.
Ocassionally a property is changed without AngularJS knowing about it. You
can manually kickstart a digest cycle vis $apply.
So, my question is what are those situations in a real web application when I need to kick off this digest cycle manually. And are those situations often seen? Kindly suggest.
This will come up any time an asynchronous callback returns from a non-angular library. e.g.
setTimeout(function() {
$scope.myVar = 1;
//Angular doesn't know when setTimeout finishes
//so you have to manually kick off a digest cycle.
$scope.$apply();
});
Angular has the $timeout service which takes care of starting a digest cycle for you but if you are using some third party library that takes a callback and doesn't have an angular wrapper then you will have to do this.
These situations can happen when using 3rd party libraries which provide some kind of data for example.
Say you use library-X which fires an event when something happened and new data is available, which you would like to render with AngularJS.
In these causes AngularJS does not know that data in the scope changed if you just directly set the variables.
That is why you should only modify scope variables inside the $apply function:
function MyController($scope) {
$scope.load = function() {
$scope.message = 'Loading...';
setTimeout(function() {
$scope.$apply(function () {
$scope.message = 'Finished loading!';
});
}, 2000);
}
}
It is also advised to use $scope.$apply(function () { /* update code */ }) instead of the single $scope.$apply() call, since it will properly catch errors and run the diggest regardless of any errors.

Why are these two updates no reflected in the view?

I'm trying to understand how to properly manipulate properties via a controller. The following code executes six updates over four seconds. Updates two and three are not reflected in the view. Why is this, and what do I need to do to have updates of those types affect the view?
Html
<div ng-controller="Controller">
myValue: <span ng-bind="myValue"></span>
</div>
Javascript
var app = angular.module('myApp', []);
app.controller('Controller', function ($scope, $interval) {
$scope.myValue = "first";
console.log($scope.myValue);
setTimeout(function() {
$scope.myValue = "second"; // never updates
console.log($scope.myValue);
$scope.$emit("my-event", "third"); // never updates
console.log($scope.myValue);
$interval(function() {
$scope.$emit('my-event', "fourth");
}, 1000, 1);
}, 1000);
$interval(function() {
$scope.myValue = "fifth";
console.log($scope.myValue);
$interval(function() {
$scope.$emit("my-event", "sixth");
}, 1000, 1);
}, 3000, 1);
$scope.$on('my-event', function (event, arg) {
$scope.myValue = arg;
console.log(arg);
});
});
JSFiddle
Use $timeout instead of setTimeout to opt-in to the digest cycle. second won't show since the turn of the digest cycle overrides the value of myValue.
Updated fiddle: https://jsfiddle.net/d9gbpddy/4/
You can try {{myValue}} instead of a <span> element
So I obviously wasn't clear enough in the original question, as the upvoted answer (correctly) suggests using $timeout rather than setTimeout, however the original intent was to understand why the updates were not being reflected in the view, and what could be done to have these types of updates (that originate outside angular) affect the view as was intended.
Read the Scope guide
So whilst I chose to skip the Scopes section of the developer guide because it looked to be the most boring, it was probably the most important, and it clearly points out some items imperative to understanding how angular binds data, notably the Scope Life Cycle which notes;
When the browser calls into JavaScript the code executes outside the
Angular execution context, which means that Angular is unaware of
model modifications. To properly process model modifications the
execution has to enter the Angular execution context using the $apply
method. Only model modifications which execute inside the $apply
method will be properly accounted for by Angular.
There's an excellent answer here that further explains this concept. The first setence aptly reiterates the importance of understanding scope:
You need to be aware about how Angular works in order to understand
it.
Don't just call $scope.$apply
So you start adding calls to $scope.$apply around the place to cater for these things that originate outside angular, but then eventually you start getting:
Error: $digest already in progress
Which means you can't call $scope.$apply whilst $digest is executing. After which you may think, well how can I conditionally call $scope.$apply based on whether the $digest is currently running. But, you don't need to do that...
Just use $timeout
Hah, like the upvoted answer, I know, but based on a different thought process I think. See this answer. $timeout is not just being used in place of setTimeout, but rather is being used (without a delay) to wrap any model updates that are called from outside the Scope Life Cycle, and doing so ensures no conflict with any currently processing $digest.
Wrapping up
So, in the original code snippet, the second and third updates are not reflected in the view because they are performed outside the Angular execution context. The fact that third update doesn't affect the model also means that calling events outside the execution context doesn't get you into the execution context either.
The fourth update is already wrapped inside $interval, which itself causes the updates that code to be run on the next digest. Therefore, updating the code to show an example of an event outside the angular execution context that causes its updates to be shown in the view is as follows:
setTimeout(function() {
$timeout(function({ // start wrap
$scope.myValue = "second"; // now this updates!
console.log($scope.myValue);
$scope.$emit("my-event", "third"); // now this updates!
})); // end wrap
console.log($scope.myValue);
$interval(function() {
$scope.$emit('my-event', "fourth");
}, 1000, 1);
}, 1000);

Should I remove Angular watches?

I'm developing a search engine inside angular view and I was wondering if search engine is used too many times and the user does not leave the search engine's view, how should I do to avoid the problem of overloading scope?
When I carry out a lot of searches, I can notice the view is slower. I think this problem is caused by scope overload, but I'm not sure.
An example:
If I get the results of a request into $scope.variable1 and after I make another request again and overwrite $scope.variable1...what happen? are watchers of data structure inside old variable1 removed automatically?
In summary, sometimes when I use too many times an angular views without leaving it the view is slowed down. Which is the best practice to deal with it?
Anytime a scope variable changes a digest cycle is triggered meaning all watchers are checked to see if anything has changed. ( actually 2 times for dirty checking ). In a search field you should throttle how many times you update your scope variable otherwise the digest cycle will kick in too much. you can do this with
debounce
<input type="text" name="variable1"
ng-model="variable1"
ng-model-options="{ debounce: 1000 }" />
Also make sure you are not creating a new watcher when $scope.variable1 changes. declare the watcher once in your controller
Anytime you use:
{{variable}}
you are implicitely creating a watcher on that page.
Whenever your page contains more than 2000 watchers you will see slowing of the page because the digest cycle will take to long for it to be snappy
you can use this snippet to count the number of watchers on your page :
(function () {
var root = angular.element(document.getElementsByTagName('body'));
var watchers = [];
var f = function (element) {
angular.forEach(['$scope', '$isolateScope'], function (scopeProperty) {
if (element.data() && element.data().hasOwnProperty(scopeProperty)) {
angular.forEach(element.data()[scopeProperty].$$watchers, function (watcher) {
watchers.push(watcher);
});
}
});
angular.forEach(element.children(), function (childElement) {
f(angular.element(childElement));
});
};
f(root);
// Remove duplicate watchers
var watchersWithoutDuplicates = [];
angular.forEach(watchers, function(item) {
if(watchersWithoutDuplicates.indexOf(item) < 0) {
watchersWithoutDuplicates.push(item);
}
});
console.log(watchersWithoutDuplicates.length);
})();
if to many watches exist consider using:
{{::variable}}
this will create a one time binding and eliminate some watchers on your page.
Another tip is to use pagination for your search results, this will also limit the amount of watchers on your page
and lastely you probably shouldnt use watchers to begin with.
read this article:
probably dont need watchers
Kind regards,

AngularJs: double check in ng-show

Why checks of any expression in directives like ng-show (or other) are duplicate? I made simple jsfiddle example http://jsfiddle.net/fA5YX/. See in browser console.
<div ng-app ng-controller="ctrl" ng-show="test()">shown</div>
<script>
function ctrl($scope) {
$scope.test = function() {
console.log('check');
return true;
}
}
</script>
How make it stop? I need just one check.
Perhaps linked to Controller function getting called twice using ng-show - to quote:
At each digest cycle, for every watch, AngularJS evaluates the
associated expression to see if there's any change and if there is,
invoking the listener (in the case of ng-show/ng-hide, the listener
will show or hide the element based on the value returned by ready()).
Every watcher is run at the digest cycle. The digest cycle is repeated until none of the results has changed value (or when angular guards against infinite loop). This is why your watchers are run multiple times. Bottom line is, don't rely on watchers being fired only once because this goes against the basic groundrules of why angular works.
If you post details on what you try to achieve we might be able to guide you to a pattern that actually works in the angular philosophy.

Categories

Resources