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

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

Related

Angular digest apply when setValidation

When I submit a form I do some backend validation to ensure that an email is unique as follows:
$http.post('/api/users.json', dataObj)
.then (function() {
// we are happy
})
.catch(function(response) {
if(response.data.data.exception) {
var emailNotUniqueException = 'App\\Http\\Exception\\EmailNotUniqueException';
$scope.userRegister.email.$setValidity("unique", false);
scrollToFirstError();
}
}
);
The scrollToFirstError does what is says on the tin:
function scrollToFirstError () {
var invalidInputs = angular.element('input.ng-invalid, select.ng-invalid');
return invalidInputs.first().focus();
}
The problem of course is that although I can see ng-valid has been applied to the input the binding is lost I believe. I read about scope digest and apply, if I add either I get a message:
angular.js:14328 Error: [$rootScope:inprog] $digest already in progress
What am I doing wrong?
After you set the validity of a field to invalid, you have to wait until the current digest cycle is applied. If you don't do so, the HTML might be not updated yet when you look for ng-invalid classes.
That being said, it is often a good idea to let the digests finish themselves and do not start or invoke them by hand, unless you really know you need this.
In this particular case, I would suggest you to call scrollToFirstError in the next digest cycle. It can be simply achieved with the $timeout service. Inject it first and then replace your scrollToFirstError() call with the following:
$timeout(scrollToFirstError);

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!

$watch fires on focus lost

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.

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 - streamline form (automatic) submission based on dirty scope

Problem space
I have a problem where I'm submitting a form based on criteria being fulfilled, rather than having a form submission button.
Let's say I have 3 drop downs, the first two are grouped but one needs to be selected, meaning I can select one or the other but I can't leave them empty, the 3rd one is a required field.
After that, the page automatically fetches in results.
Lets say I have checkboxes and a few more dropdowns. Any future selections on the 3 dropdowns mentioned, checkboxes, and dropdowns automatically filters the results.
What I know
Now after reading angular documentation, I was checking up on $dirty, $pristine and operations on both, like $setDirty and $setPristine; however, it seems that this is for a FormController
So I'm assuming this is useful for an entire scope. I didn't see any inclination that I can figure out for selected scopes.
What I have so far
So basically, I was hoping that I'd be making use of the scope's tracking features, but I don't know much about it. I created a single controller for my application and a single scope, since that's what seemed easiest for me. I have 3rd party plugins that play a role into the scope like:
$scope.3rdpartyConfig = {
prop1: [],
prop2: getData()
}
I don't think something like that would be useful in checking to see form submission if I was going to check the $dirty state of my form.
Then I thought about the old way I used to do things, but "angularlizing" it:
so I'd have something like:
<input type="checkbox" ng-model="state.Checked" ng-change="checkIfWeCanSubmitThenSubmit()" id="ng-change-example1" />
So I'd be having ng-changes and ng-clicks all over my html form, hitting that function, where the function would look like this pseudocode:
$scope.checkIfWeCanSubmitThenSubmit= function() {
var validated = false;
//check to see if dropdown1 or dropdown2 are selected
//check to see if dropdown3 is selected
// add more here per requirement
//if the above are true, then validated = true
if (validated)
{
//add dropdown4 and 5, and checkbox groups into filter
}
submit();
}
But I was thinking this isn't the angular way of doing things since this certainly isn't facilitated.
I was hoping that the scope would offer some kind of way, where I can check to see what pieces of my scope is dirty or not before I can submit and fetch data, or if there is a better way than appending this function to every html element; like having some kind of scope tracker that I can check up on and watch.
Which reminds me, I don't want to have a series of $scope.$watch either, its just that it'd be way too much work to bind to every piece of html code, unless there's way to watch the scope of a collection of specific scope variables, then, I wouldn't mind.
like (forgive the pseudocode):
$scope.$watch('dropdown1, dropdown2, dropdown4', function(dirty, pristine)
{
if (dirty)
{ blah blah blah }
});
Edit (2/28/2013):
I tried doing it this way:
$scope.masterCriteria =
[
{ DropDown1: $scope.AppModel.Dropdown1},
{ DropDown2: $scope.AppModel.Dropdown2 },
{ DropDown3: $scope.AppModel.Dropdown3 },
{ Checkbox1: $scope.AppModel.Checkbox1 },
{ Checkbox2: $scope.AppModel.Checkbox2 }
];
$scope.$watch('masterCriteria', function (newVal) {
if (newVal) { logger.info("did I change?"); }
}, true);
The watcher detected nothing, and any values I changed to the scope of AppModel wasn't being picked up in the $watch. Was worth a try, still trying to figure this out.
You can slightly change your model and group fields related to input form together. Put them into single object. Like this:
$scope.state = { checkbox1: false, checkbox2: true, ... }
Later bind input boxes to field of state object:
<input ng-model="state.checkbox1" ... >
And watch state object to catch all updates of nested fields:
$scope.$watch('state', ...
JsFiddle example here

Resources