Angular digest apply when setValidation - angularjs

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);

Related

AngularJS Why is this controller variable not getting updated in the UI?

I'm having a controller using StompJS to subscribe to a url (back-end is Spring Java) that returns an alternating string "list" and "box" every 5 seconds. I want to update my UI element when StompJS receives some data, but I couldn't get the UI element to update. I've test the same logic with a $timeout and the UI is getting updated so it must has something to do with the way callback function works. Can anyone see what is the reason UI is not updating?
I have these simple UI elements:
<input ng-model="ctrl.uniqueId"/>
<input ng-model="test"/>
ctrl.uniqueId is to verify whether the actual controller instance is being updated. For some reason, only 1 controller is making 5 different subscribes every time. If someone can help with that, it'd be great too but I doubt you can get much info unless you see all my codes setup.
Anyway, in my controller (tried self.test and it didn't work so I tried with $scope.test to see if it makes a difference):
self.uniqueId = window.performance.now();
$scope.test = 'list';
// the UI will be updated to dummy after 3 seconds.
$timeout(function() {
$scope.test="dummy";
}, 3000);
// the UI will not update.
var callBackFn = function(progress) {
$scope.test = progress;
console.log(self.uniqueId + ": " + $scope.test);
};
// the server returns alternating data (list and box) every 5 seconds
MyService.subscribeForUpdate('/topic/progressResults', callBackFn);
This is my service's code for StompJS if that matters:
self.subscribeForUpdate = function(channelUrl, callBackFn) {
self.socket.stomp.connect({}, function() {
self.socket.subscription = self.socket.stomp.subscribe(channelUrl,
function (result) {
//return angular.fromJson(result.body);
callBackFn(result.body);
return result.body;
}
);
});
};
This is console.log results:
1831.255000026431: list
1831.255000026431: box
Extra: is it possible to get the return data without callback function similar to Promise?
Be sure to use $apply:
app.service("myService", function($rootScope) {
var self = this;
self.subscribeForUpdate = function(channelUrl, callBackFn) {
self.socket.stomp.connect({}, function() {
self.socket.subscription = self.socket.stomp.subscribe(channelUrl,
function (result) {
//return angular.fromJson(result.body);
$rootScope.$apply(function() {
callBackFn(result.body);
});
return result.body;
}
);
});
};
})
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc... You can also use $apply() to enter the AngularJS execution context from JavaScript.
Keep in mind that in most places (controllers, services) $apply has already been called for you by the directive which is handling the event. An explicit call to $apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.
For more information, see
AngularJS Developer Guide - Integration with the browser event loop
This is a very common issue and happens when a 3rd-party library(out of the angular environment) is used with angularjs. In such cases you need to manually trigger a digest cycle using the:
$scope.$apply()
After that all angular bindings will be updated. Using $timeout (even without timeValue) has the same result as it also triggers $apply()

AngularJS: Nested http call doesn't update the view

In a particular scenario, I need to call the the github api to retrieve a specific user's info. Then issue a second call to retrieve the user's repositories:
search(login: string): void {
this.user = undefined;
// first call
this.githubApi.getUser(login)
.then(user => {
this.user = user;
// second call
this.githubApi.getRepos(user.repos_url)
.then(reposResponse => {
this.repos = reposResponse.repos;
// I don't like to call this.$scope.$apply() !!;
});
});
}
The first call gets executed and the bound elements to this.user gets updated with no problem in the view.
The second call gets executed and the result is returned successfully and this.repos is set correctly. But, the bound elements on the view are not updated.
If I call this.$scope.$apply() in the very last line of the second callback, it makes the view update work but I guess this is not correct approach.
Any solution?
Well, if you are not willing to use $scope.apply();, try updating your getRepos service response code with:
setTimeout(
() => {
this.repos = reposResponse.repos;
}, 0
)
First you need to know , why Angular-Js is not updating the view.
You have used $scope.$apply(), so I'm assuming you already know , how it works and why we use it. Now , to the problem!
Sometimes when you make a callback - nested callback in particular - Angular does not update the view. Sometimes angular thinks that it does not need to update the view because of callbacks. And the watchers do not take action when the value changes of the variable that they are watching.
Then you use $scope.$apply() to run the digest cycle again (assuming you already know the digest cycle if you don't then let me know). And it makes the watchers to update the view.In your case, digest cycle is not running, that is why angular is not updating the view. If your digest cycle was running , angular would have given you error. So, it will tell angular to run digest cycle again because two-way binding is not working properly.
I don't think there is another way. But if there is a way, I would love to know that way. Also its not a bad approach. It was made for these kind of problems.

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

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 : What's the Angular way to interact with a form?

I understand one of the key principals of Angular is:
Thou shalt not reference thy DOM from withinst thou's controllers.
I'm trying to process a credit card payment, which requires the following steps:
User fills out a form, and clicks a submit button
A portion of that form is sent to our servers, which starts a transaction with the payment gateway
The response from our servers updates values in the form, which must then be submitted directly to the payment gateway, via a form POST.
Other stuff happens.
In this scenario, how do I:
Update the data in the form (without referencing the form from the controller)
Get the form to submit?
The form binds to a model on my controller, so I've tried something like the following:
<form action="{{paymentModel.urlFromTheResponse}}">
<input type="hidden" name="accessCode" value="{{paymentModelaccessCodeFromResponse}}" />
<button ng-click="startTransaction(paymentModel)"></button>
</form>
// in my success handler
.success(function(data) {
paymentModel.urlFromTheResponse = data.url;
paymentModel.accessCode = data.accessCode;
$scope.apply();
}
the theory being here that if I can immediately get the form into the correct state via databinding, I can then do something to submit the form. However, this throws an error:
Digest already in progress
What's the Angular way to support this type of flow? It seems I'm required to interact directly with the DOM, which goes against the nature of controllers.
As others have stated, you shouldn't need to call $scope.$apply() because the form should already be tied to angular by setting ng-model attributes on each of the fields.
However, occasionally it is necessary to call $scope.$apply() to update display when data is pulled in from some other source outside of angular...
In those cases, I've had great luck with this:
// This method will be inherited by all other controllers
// It should be used any time that $scope.$apply() would
// otherwise be used.
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
I place that in my outermost controller, so all other controllers on the page inherit the function from it.. Any time I find I need a call to apply, I instead call $scope.safeApply() which will call apply if there is not already an apply or digest in progress, otherwise, those changes will already be picked up by the currently running apply/digest.
In your code I would change this:
<input type="hidden" name="accessCode" value="{{paymentModelaccessCodeFromResponse}}" />
To this:
<input type="hidden" name="accessCode" ng-model="paymentModel.accessCode" />
I would probably also remove the form action, and instead add something like this in the controller:
$scope.$watch('paymentModel.accessCode', function() {
// Fire off additional form submission here.
})
The error is generated because your success callback is already "inside Angular", so $scope.apply() will be called automatically for you.
If you use ng-model (instead of value) on your form elements, then you can modify the model/$scope properties in your success callback and the form will automatically update (due to two-way databinding via ng-model). However, instead of trying to submit the form, why not just use the $http or $resource service inside your controller to call the web service? (That's why I asked if the user needed to be involved in my comment.)
Assuming you are using something like $http, you are already inside of the angular scope and should not need to manually call $scope.apply(); as you are inside of the angular execution already.
You should be able to ditch the $scope.apply() and simply have an
.success(function(data) {
paymentModel.urlFromTheResponse = data.url;
paymentModel.accessCode = data.accessCode;
$http.post("/finalstep",paymentModel).success(function(data)
{
// other stuff
});
}

Resources