Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Add a way to handle $asyncValidators completion #10768

Open
brian428 opened this issue Jan 15, 2015 · 18 comments
Open

Add a way to handle $asyncValidators completion #10768

brian428 opened this issue Jan 15, 2015 · 18 comments

Comments

@brian428
Copy link

The new $asyncValidators are a nice addition. But there seems to be a critical missing piece here: there doesn't seem to be a way to react when the $asyncValidators complete their work.

The documentation and blog entries I've been able to find will occasionally mention that you can add a check for $pending to a submit button to prevent submission. But that seems to be a really constrained option, since it offers no programmatic control.

The only option I could come up with was to check if $pending is present on the form scope, and add a watch on it. When $pending is removed, I can clear the watch and trigger additional logic. This works, but it seems harder than it should be.

My thought is to add a "promise" key to the $pending object, that provides a Promise around the async validation logic. This way, we would be able to do $pending.promise.then( ... ) to react when the async validation is complete.

@Narretz
Copy link
Contributor

Narretz commented Jan 15, 2015

We are working on a bigger refactor of ngModel that will introduce a hook into the end of a validation run - after all validators have run, basically. Would this suffice for your situation?
Also, what about:

ngModel.$asyncValidators.async = function() {  
  var deferred = $q.defer();
  var promise = $deffered promise;

  promise.then(function(result) {
    // handle the resolved asyncValidator here
  }) 

  //Do your magic ...
  deferred.resolve(true');

  return promise;
}

@brian428
Copy link
Author

The ngModel tweak and the return of a promise from the validation function are fine if you just care about knowing when a single async validator is finished. (EDIT: I really mean a single field, since a field can have multiple async validators.) But I'm working at the level of the overall form, so I need to know when all of the async validators (for all the fields in the form) are done. That's why I was thinking that the $pending object itself could have a promise key, to store a Promise that resolves once all of the async validators are finished. Hopefully this makes sense.

@Narretz
Copy link
Contributor

Narretz commented Jan 19, 2015

@petebacondarwin I think this is something we should keep in mind for the ngModel refactor / as a second step.

@petebacondarwin
Copy link
Contributor

This is in my POC.

For a single validation to complete see: https://github.com/petebacondarwin/ngModelPOC/blob/master/src/Validity.js#L18-L58
And for handling multiple (potentially out of date) validations see: https://github.com/petebacondarwin/ngModelPOC/blob/master/src/ngModelAdaptors.js#L20-L53

@brian428
Copy link
Author

I think that looks good. Can I infer that the promise will end up as a key on the form's $pending object?

@petebacondarwin
Copy link
Contributor

This is up for grabs. We should have a discussion about how best to represent this. I am thinking more explicit $pendingValidation might be more intuitive?

@brian428
Copy link
Author

Hmm...from what I can see, the existing $pending object contains keys for each of the async validators in use, correct? If so, I'd say just adding another key to contain the promise would seem to make sense, vs. adding a $pendingValidation to the form object. (At least I'm assuming it would go there, alongside $pending?)

That said, I really don't have a strong preference either way. As long as I have a stable way to know when the async validators are finished, I'll be happy.

@matsko
Copy link
Contributor

matsko commented Jan 22, 2015

For now, can't you just place a watcher on ngModel.$pending?

@brian428
Copy link
Author

As I said in my initial post, that's what I'm doing now. It's just not a very simple/obvious/intuitive solution for something this fundamental to form validation. Since the async validation functions already return promises, wrapping them up inside another promise should be pretty straightforward. It's also much more elegant and in line with how async processes should be handled in general. This is what promises are for, after all. :-)

@intellix
Copy link
Contributor

+1 to this. At the moment I have <form ng-submit="next(form)"> and in my controller:

$scope.next = function(form)
{
  if (form.$valid) {
    $scope.process.step += 1;
  }
};

But as said above, the form might be in the process of resolving it's asyncValidators, so $pending contains stuff. Maybe it would be cool if I could do something like:

$scope.next = function(form)
{
  form.$asyncValid.then(function() {
    $scope.process.step += 1;
  });
};

Right now I'm doing:

$scope.next = function(form)
{
    if (form.$pending) {
      var pendingWatch = $scope.$watch(function() {
        return form.$pending;
      }, function(pending) {
        if (!pending) {
          pendingWatch();
          $scope.next(form);
        }
      });
    }
    if (form.$valid) {
      $scope.process.step += 1;
    }
};

@danicomas
Copy link

@intellix +1

@petebacondarwin
Copy link
Contributor

@intellix - actually I am concerned that this design of your app would add complexity and cause the user problems. For example, if the user clicks submit but that the async validation fails then you would have to:

  • indicate to the user that, although they have tried to submit the form, that you are waiting for the pending validation to complete
  • react when the validation fails and inform the user that their form submission failed

It would seem to me much simpler to set form submission to disabled while $pending is true and indicate so in the UI of the form.

@intellix
Copy link
Contributor

I have the loading/thinking state handled as well but didn't include it inside the snippet. I guess what you're saying is to just outright disable form submission on pending. I could do that instead :)

@petebacondarwin
Copy link
Contributor

That is what I mean. That removes the need for all this logic.

@webJose
Copy link

webJose commented May 24, 2017

Ok, the last comment is from over a year ago. Is there any stable solution for this? I just wrote an entire piece of code to handle the scenario just to find out that what ngModel.$promise contains is just Boolean values. I thought they were the promises returned by the validators!

Anyway, I shall leave my code here in case it serves as inspiration for improvement. My code assumes that ngModel.$pending will contain key/value pairs of type validationKey/promise. The second the promise resolves, the pair is removed from $pending.

Basically I am creating a deferred object that will be resolved as soon as a validator deems the value invalid, or after all pending validators finish. I guess this logic can be used for the fom/ngForm controller to provide the promise of this deferred object somehow to the consumer.

            var deferred = ngQ.defer();
            var totalAsync = 0;
            var totalRemaining = 0;
            ng.forEach(this.Data.Form.$pending, function(controls) {
                ng.forEach(controls, function(control) {
                    ng.forEach(control.$pending, function(promise) {
                        var index = totalAsync++;
                        c.log('Promise:  %o', promise);
                        promise.then(function() {
                            deferred.notify({ index: index, result: true });
                            if (--totalRemaining <= 0) {
                                deferred.resolve();
                            }
                        }
                        , function() {
                            deferred.notify({ index: index, result: false });
                            deferred.reject();
                        });
                    });
                });
            });

EDIT In case anybody like me stumbles upon this thread by looking for a solution, I worked around the issue by encapsulating an interval check on form/ngForm.$pending. Once the object has no controls listed the deferred object is resolved; if the specified timeout elapses, the deferred object is rejected.

I share here as well. Oh, and it is worth mentioning that I have this in an attribute-only directive that extends the form/ngForm controller with the method.

And I know you hardcore JavaScripters like camelCase, but I am a strong .net player and I like my PascalCase. :-)

        function _WaitOnPendingValidators(abortTimeout) {
            var deferred = ngQ.defer();
            var form = this;
            var totalAsyncFn = function() {
                var totalAsync = 0;
                ng.forEach(form.$pending, function(controls) {
                    ++totalAsync;
                });
                return totalAsync;
            }
            //If there are no async validators pending, resolve immediately.
            if (totalAsyncFn() == 0) {
                deferred.resolve();
            } else {
                var intervalPromise = interval(function() {
                    var total = totalAsyncFn();
                    deferred.notify(total);
                    if (total == 0) {
                        interval.cancel(intervalPromise);
                        deferred.resolve();
                    }
                }, 250);
                intervalPromise.then(null, null, function(times) {
                    c.log('Checking for pending asynchronous validators.  Run # %i.', times);
                });
                if (abortTimeout && abortTimeout > 0) {
                    timeout(function() {
                        //Abort operation.
                        interval.cancel(intervalPromise);
                        deferred.reject();
                    }, abortTimeout);
                }
            }
            return deferred.promise;
        }

I attach this function to the form controller and therefore the this pointer is the form controller. I do not like using $ for my variables so the ngQ you see there is the $q service. Also c is window.console. I inject it to the IIFE for quick access. I also use $interval and $timeout in the variables interval and timeout respectively.

@petebacondarwin
Copy link
Contributor

I edited your comment @webJose - you needed to use triple backticks for a block of code.

@webJose
Copy link

webJose commented May 25, 2017

@petebacondarwin , thanks for the help. Do you happen to know if there is a solution for this already? Maybe I need to move to a newer AngularJS? I have been avoiding this in my current project because there is so much else to do to add, on top of all, potential errors introduced by moving to a new version. My current version is 1.5.3, and the guy who did this project only knew that AngularJS existed, that it had ng-model and that it could show or hide stuff. That is it. All AngularJS code written in a single file for 6 different HTML pages, manually fighting model changes and validation changes. Almost 9000 lines of code. My first cleanup reduced the size by half but I am a long way to go still. :-(

@petebacondarwin
Copy link
Contributor

There is no generic solution built into any version of AngularJS at this time. Sorry @webJose. What you are doing appears to be a reasonable approach for your app.
Good luck with the refactoring.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants