AngularJS: run after digest loop with $timeout (Safari vs chrome) - javascript

I've written some custom validation code when filling out a form, but need to set the $timeout to > 100 ms in order to get it to work, and I'm curious to find out why.
The required form elements follow this format. I add the "novalidate" class if the input is wrong: (Observe, there's a ton of these elements in the form)
<div class="dottedWrapper" ng-class="{novalidate:(newController.jobDescription.length == 0) && newController.formSubmitted}">
When submitting the form, I check if any elements has the "novalidate" class and return.
self.formSubmitted = true;
$timeout(function () {
// Validate!
if (document.getElementsByClassName("novalidate").length > 0) {
return;
}
...
}, 100); // MUST SET AT LEAST 100ms for it to work in safari
However, this only works if I set the time to about over 100ms in Safari. In chrome, It's only necessary to set it to 1ms.
Correct me if I'm wrong, but here's my thinking, and bare in mind that I'm a newbie in angular:
I set the "self.formSubmitted = true;" in the beginning. This causes, through the two-way data binding an update on all the divs, since the "formSubmitted" is contained inside the ng-class.
This is done through a digest loop, which means I cannot run the "document.getElementsByClassName("novalidate")" directly after since the digest loop most run through once and update everything.
So... I use the $timeout, thinking it will let the current digest loop run, and then jump on the next digest loop. At this point, all elements should be updated.

You're breaking the MVC pattern. Don't - it's bad practice.
Why don't you just do:
if (self.jobDescription.length == 0 && self.formSubmitted)
return;

Aside from Pixelbits answer (which I totally agree with), you could try replacing $timeout with one of the following;
$evalAsync
$$postDigest
Where the former will trigger a new $digest loop (just like $timeout), and the latter will not.
Even so, I think your best bet would be to harness the built-in input validation directives. And instead of checking for the existence of a novalidate class in the DOM, just check the <form>.$valid property (<form> gets exposed on your $scope, when given a name attribute).
Your view would look something like so;
<form name="myForm">
<input ng-model="formData.jobDescription" ng-minlength="1">
</form>
And then in your controller;
// var form = $scope.myForm;
console.log(form.$valid); // true|false
Now, the above suggestion may not work for your use case - We need to see more of your implementation to be able to assist you to a greater extent.

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.

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,

How to animate unchanged ng-repeat with AngularJS

I have a template that looks like this:
<p ng-repeat="item in myobj.items" class="toAnimate">{{item}}</p>
and I would like to use the animate module do a jQueryUI addClass/removeClass animation on the element using the JavaScript method described in the docs:
ngModule.animation('.toAnimate', function() {
return {
enter: function(element) {
element.addClass('pulse').removeClass('pulse', 2000);
}
};
});
This works beautifully, but the problem is that, since I want to use the p.toAnimate element to display status messages, it will not change the content according to angular.
To break it down a little further, say I have a name field. When I click Save the message Name was saved successfully. is displayed. Now if I modify the name and click save again, assuming the save was successful, the message should be re-displayed to give the user feedback of the newly edited name. The pulse does not happen, however, because the items in myobj.items didn't technically change.
I realize that I could remove the item after a period of time (and that is probably the route I will take to implement the real solution), but I'm still interested to see if this sort of thing can be done using AngularJS.
What I want to do is register with angular that the message should be treated as new even though it is not. Is there any way to do this?
A fiddle to go along with this: http://jsfiddle.net/Jw3AT/
UPDATE
There is a problem with the $scope.$$phase approach in my answer, so I'm still looking for the "right" way to do this. Basically, $scope.$$phase is always returning $digest, which causes the conditional to fail. Removing the conditional gives the correct result in the interface, but throws a $rootScope:inprog.
One solution I found is to add a $apply in the middle of the controller function:
$scope.updateThingy = function () {
$scope.myobj.items = [];
if (!$scope.$$phase) {
$scope.$apply();
}
$scope.myobj.items = ['Your name was updated.'];
};
Updated fiddle: http://jsfiddle.net/744Rv/
May not be the best way, but it's an answer.

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.

Unit test controller that depends on form validation in AngularJS

In my controller I want to invoke an action (say on Tab press) only when form is valid. Also I need to clear form as soon as form gets submitted succesfully. I have something like this
app.controller('CommentFormController', function($scope) {
$scope.submit = function() {
if($scope.commentForm.$valid) {
// submit form
$scope.comment = '';
$scope.commentForm.$setPristine();
}
}
});
I'd like to test this, but it looks like I have to create this $scope.contactForm by hand and stub out $setPristine() function.
Is there any other way to test it? I mean can I somehow get instance of underlying FormController in my test?
How do you handle such cases?
Setting the form to pristine will affect the state of the form but won't reset the form to your defaults values (if you've provided them). Instead you can use the DOM element method reset().
Something like this:
document.getElementById("yourform").reset();
or, since angularJS and jQuery play nicely, you can use css selectors (especially useful if you have multiple forms you want to clear at once.
So something like:
$("#yourform")[0].reset();
There are pure javascript ways to do it also:
http://www.javascript-coder.com/javascript-form/javascript-reset-form.phtml
--- So in summary, you don't need to use specific methods to do this, simply use the DOM methods, jQuery, or pure javascript. Google will probably come out with a way to do this soon. Hope this helps.
#grafthez I got same problem when try validate the form is valid in my controller by $scope.myForm.$valid.
I found a solution on https://stackoverflow.com/a/17129354. You can try to inject $compile then $compile(yourFormTemplate)($scope).

Categories

Resources