$watch fires on focus lost - angularjs

I have a textarea tag with jquery.nicescroll pluggin and ng-model attached to it.
<textarea id="paper" ng-model="paper"></textarea>
In my code I apply a $watch on this ng-model variable.
$scope.$watch("paper", onTextChange);
Everything is good except that onTextChange is fired not only when I type something, but when I click away from textareaб and also when I switch to another tab.
How can I prevent it so that onTextChange is fired only when the text is changed, meaning when I type in something or delete chars?
Demo with instructions: plunker

here's a fix:
http://plnkr.co/edit/kycmUrthYU38Ukdz0jJG?p=preview
setTimeout(
function() {
$scope.$watch("paper", function(newtext, oldtext) {
if (newtext !== oldtext) {
onTextChange();
}
});
}, 100)
So the issue is that watch fires the function whenever angularjs tells the app to digest. What you did was tell it to call the 'change' function EVERY time, when you should have passed in a checker function to check the change happened. It's about 'watching', not about 'watching for changes' - the function argument is supposed to see if you need to do something.
Extra note:
AngularJS sets up watchers for all kinds of things on various elements - here's a bit more info. I believe the blur corresponds to ng-touched which triggers the digest What gets added to $scope.$$watchers by default in Angular? And what triggers $digests?

It is a bad idea to use setTimeout. Instead, you could use ng-model-options="{ getterSetter: true }" and write a method to get/set the value (Modified get/set Plunk) and handle the text change condition within this method.

Related

Angular, wait until the digest cycle completes before returning from function?

I would like to highlight a field in red if the user tries to submit the form while the field is empty. So I set a showErrors boolean to true on submit, and conditionally require my field based on this value ng-required='showErrors'.
I define a isValid function which triggers a digest cycle to allow the showErrors value to propagate, but the function is not behaving as I would like. With the field empty, the first time the submit button is clicked isValid returns true and then false on subsequent clicks, but I would like it to always return false if the field is empty.
How can I force my isValid function to wait for the digest cycle to propagate prior to returning validity?
https://plnkr.co/edit/wH6HfawquHJwR69AUPRI
From personal experience it would be easier to control the hightlight by using css on the form level.
ng-required is reserved for use when we need to set required dynamically, for example user can choose to input phone or email, then required for email input field is only needed when user select email.
<form ng-class='{showErrors: showErrors}'>
<input required />
</form>
.showErrors .ng-invalid {
border-color: red;
}
TlDr; use $timeout(function(){...});
The answer provided by #Icycool does do the trick in this use case. But the reason i came to this post was the reason of the title of the post: "How to wait till the $digest cycle is done?"
I did some searching and found this post on coding-geek explaining the different elements that come to hand in the $digest cycle.
It explains that $timeout can be used to wait a cycle, (or more, if you add the timeout parameter) It also says that you can use $evalAsync but this does not always do the trick for me.
So here is an example where a scope variable is watched and a input element is triggered. It's kind of abstract and psuedo because I do not have a good use case right now.
$scope.$watch('vm.foo', function(newFoo) {
if (newFoo) {
$timeout(function() {
$window.alert('new Foo set!' + newFoo);
});
} else {
$timeout(function() {
$window.alert('Foo was removed!');
angular.element('input').focus();
}, 2000); // waits for 2 secs before alert and focus
}
});
I've found this plunker that uses plain timeout's and $apply, but you can fiddle around with it to make it work for your use case, i found it helping to understand cycles: https://plnkr.co/edit/1nzydOS7AVONmXh9?preview

Strange behaviour with ng-model binding and checkbox

I'm trying to intercept a checkbox change so I can put a confirmation stage in the middle and I am experiencing strange behaviour.
When I click the checkbox preventDefault is stopping the UI from changing the checkbox, except the bound model will change once and then no longer be changeable.
Any ideas on how I can fix this? Am I approaching this wrong?
$scope.change = function(selected, $event){
$event.preventDefault();
};
https://jsfiddle.net/tcVhN/197/
edit: Answers to JB's questions below:
I am trying to intercept the checkbox change so I can put a confirmation step in the middle IE "Are you sure you want to change
this text box?"
Just updated to 1.47 (and updated jsfiddle link).
I'm using ng-click because ng-change doesn't pass the event
through which means I can't cancel the ui change via
$event.preventDefault.
See above.
I have modified your Fiddle to make it work:
https://jsfiddle.net/masa671/8qrct4y2/
Notice the change in HTML: ng-model="x.checked" to ng-checked="x.checked".
JavaScript:
$scope.change = function(selected, $event){
$event.preventDefault();
$timeout(function () {
if (window.confirm('Are you sure?')) {
selected.checked = !selected.checked;
}
});
};
The key problem for me was to find out, how to prevent the checkbox status from changing until the user has confirmed the change. I don't know the best/right solution, but I resolved this so that the event handler just prevents the default behaviour, and the actual change is handled outside the event handler with the help of $timeout.
At least the Fiddle seems to work in a sane manner... :-)

Watch form model for changes

Assuming a given form such as <form name="myForm">, it's easy enough to watch for validity, error, dirty state, etc. using a simple watch:
$scope.$watch('myForm.$valid', function() {
console.log('form is valid? ', $scope.myForm.$valid);
});
However, there doesn't appear to be an easy way to watch if any given input in this form has changed. Deep watching like so, does not work:
$scope.$watch('myForm', function() {
console.log('an input has changed'); //this will never fire
}, true);
$watchCollection only goes one level deep, which means I would have to create a new watch for every input. Not ideal.
What is an elegant way to watch a form for changes on any input without having to resort to multiple watches, or placing ng-change on each input?
Concerning the possible duplicate and your comment:
The directive solution in that question works, but it's not what I had in mind (i.e. not elegant, since it requires blur in order to work).
It works if you add true as third parameter for your $watch:
$scope.$watch('myFormdata', function() {
console.log('form model has been changed');
}, true);
Further information see the docs.
Working Fiddle (check console log)
Another more angular way would be to use angular's $pristine. This boolean property will be set to false once you manipulate the form model:
Fiddle
Based on my experience with my forms (new dev, but working with Angular for a while now), the elegant way to watch a form for changes is actually not to use any type of watch statement at all actually.
Use the built-in Angular boolean $pristine or $dirty and those values will change automatically on any input field or checkbox.
The catch is: it will not change the value if you add or splice from an array which had me stumped for a while.
The best fix for me was to manually do $scope.MyForm.$setDirty(); whenever I was adding or removing from my different arrays.
Worked like a charm!

Using a variable for ng-required doesn't re-evaluate fields

I have a form where my intent is for required fields to not always be enforced. For example if the user is saving the document as a draft they can enter as little information as they like, if they try and publish the document then they have to enter all the required fields. I'm using a boolean on the controller which changes according to which button has been pressed e.g.
<input type="text" ng-model="field2" ng-required="enforceRequired" />
The problem is that the fields are not re-evaluated when the boolean changes so the form is submitted and then it becomes invalid. Please see this JSFiddle to see what I mean. If you fill in field1 and then click publish it will succeed on the first click and THEN become invalid.
How can I force the validation to run before the form is submitted?
Yarons is right, you are changing the value too late, by late I mean after the form validations has been run. What you can do as a workaround is, after changing the required value, let angular do another cycle and then show your alert. This can be done via $timeout service, although I must mention that it is usually not a good practise to change the flow of your digest actions. It gets pretty messy pretty soon.
Change your publish function like this (and don't forget to inject $timeout)
$scope.publish = function () {
$scope.enforceRequired = true;
$timeout(function () {
if ($scope.form.$valid) {
alert("Published!");
}
});
};
Working fiddle: http://jsfiddle.net/bh9q00Le/14/
The problem is that you are changing the value of enforceRequired in the middle of the digest loop, so the watchers are not re-rendered before you check the input fields' validity (read about digest here).
If you want to get around it, I suggest one of the following methods:
change the value of enforceRequired before you call saveDraft or publish. see example.
call $scope.$apply() after you change the value of enforceRequired. see another example.

Angularjs Bootstrap UI tooltip scope preventing watch from firing

I'm having an issue with a $watch that is really just boggling my brain. I'm almost certain that it is somehow a scope issue. Here's the basics. I'd like a tooltip on a span surrounding a checkbox and it's label so that the tooltip activates on hover over the checkbox or label. something like this.
(note:I've added the {{values.isChecked}} just to watch the values in the two different scopes)
HTML
{{values.isChecked}}
<span tooltip-placement="right" tooltip="This is my tooltip">
<input type="checkbox" id="myCheckbox" ng-model="values.isChecked">
<label for="myCheckbox"> My Checkbox</label> {{values.isChecked}}
</span>
from angular controller
$scope.values.isChecked = true;
$scope.watch("values.isChecked",function(newValue,oldValue){
alert("Made it to the watch");
}
The most odd behavior is that my watch catches the transition of the value from true to false. But does not catch the transition from false to true. I can click it several times and it will make it to the watch going from true to false, but not false to true. If it catches the true to false, and then catches true to false again, well, it HAD to have changed from false to true in order to trigger the watch again. I know the value is actually changing, the two places wehre I added it to the page via {{values.isChecked}}, both show the values change as expected, it's just not firing my watch when I CHECK the box. only when I UNCHECK it.
All my code is on a different box on an isolated network, so I can't actually copy and paste any, so tried to just type in the relevant stuff.
Also if I just take out the Span that has the tooltip on it, it works just fine. I'm aware that bootstrap UI's tooltip does create a new scope. so suspect that, but don't know why it works for one transition, but not the other.
I have even gone as far as capturing the scope inside the tooltip and adding my watch there such as...
myChildScope = angular.element('#myCheckBox').scope()
myChildScope.$watch("values.isChecked",function(newValue,oldValue){
...
It behaves incorrectly, the exact same way. Also behaves the exact same (bad) way if I try to add an ng-click or ng-change to the checkbox element.
Two things to try, I'm not sure how your code is setup but for issues of a watch not catching things when you think it should, generally one of these will work.
$scope.watch("values.isChecked",function(newValue,oldValue){
alert("Made it to the watch");
}, true);
The true tells it to compare for object equality using angular.equals instead of comparing for reference equality. Since it's a boolean primitive... probably more of what you want to use.
The other option, which may or may not help in your case, it to use
$scope.watchCollection("values",function(newValue,oldValue){
alert("Made it to the watch");
});
And see if anything in the values object changes.
Also, you could change isChecked to an object,
$scope.isChecked = { checked: false }
and $watch the object rather than the boolean itself.

Resources