27

I am building a reactive angular form and I'm trying to find a way to trigger all validators on submit. If the validor is a sync one, it'd be ok, as I can get the status of it inline. Otherwise, if the validator is an async one and it was not triggered yet, the form on ngSubmit method would be in pending status. I've tried to register a subscribe for the form statusChange property, but it's not triggered when I call for validation manualy with markAsTouched function.

Here's some snippets:

   //initialization of form and watching for statusChanges
   ngOnInit() {
        this.ctrlForm = new FormGroup({
            'nome': new FormControl('', Validators.required),
            'razao_social': new FormControl('', [], CustomValidators.uniqueName),
            'cnpj': new FormControl('', CustomValidators.cnpj),
        });

        this.ctrlForm.statusChanges.subscribe(
            x => console.log('Observer got a next value: ' + x),
            err => console.error('Observer got an error: ' + err),
            () => console.log('Observer got a complete notification')
        )
    }
    //called on ngSubmit
    register(ctrlForm: NgForm) {
            Forms.validateAllFormFields(this.ctrlForm);
            console.log(ctrlForm.pending); 
            //above will be true if the async validator
            //CustomValidators.uniqueName was not called during form fill.
    }
    //iterates on controls and call markAsTouched for validation,
    //which doesn't fire statusChanges
    validateAllFormFields(formGroup: FormGroup) {         
          Object.keys(formGroup.controls).forEach(field => {  
              const control = formGroup.get(field);             
              if (control instanceof FormControl) {             
                control.markAsTouched({ onlySelf: true });
              } else if (control instanceof FormGroup) {        
                this.validateAllFormFields(control);            
              }
          });
      }

Any ideas on how can I ensure that the async validator was executed so I can continue with the register logic having all validators triggered and completed?

iangoop
  • 281
  • 1
  • 4
  • 3
  • try to use this.roleForm.get("razao_social").setAsyncValidators([CustomValidators.uniqueName]) to assign the validator – Ricardo Mar 27 '18 at 14:55
  • Why not make a customValidator that return a observable, and in register call CustomValidator(ctrlForm.value).subscribe(res=>{if (res.ok) continue else showError)? – Eliseo Mar 27 '18 at 15:02
  • @Eliseo, indeed, it will solve the problem, i hadn't thought about this, but I hope there's an more automatic solution, where i shouldn't need to know the validator on the ngSubmit, using the markAsTouched or similar – iangoop Mar 27 '18 at 15:21
  • @Ricardo I don't see how can it help me to retrieve the status on the register method, if you can give me some ideas on that... – iangoop Mar 27 '18 at 15:22
  • @iangoop the idea of custom validators is be executed once the field is modified.. if your validator is not working is because two reasons: bad implementation or bad assignation to the field, the solution I give to your is for bad assignation, just try to test if at least the method has been called – Ricardo Mar 27 '18 at 15:39
  • @Ricardo the validator works, i just want to ensure that its execution is finished when I call register method. The method validateAllFormFields triggers all validators attached to FormGroup, and the response of the synchronous ones will be available as soon as I call control `markAsTouched`, and because of that i can ensure that the form is valid. But with the asynchronous validator the scenario changes, and i'm willing to found a automatic and formal way to wait the form looses its pending status so I can check its validity – iangoop Mar 27 '18 at 15:58
  • Hehh... Have you tried setting a timeout on the async validator and then tried to submit? I have, and I can not submit before the Promise is complete... I use `this.form.valid` to check that the form is valid before I can go any further. Works for me! – ravo10 Mar 18 '19 at 05:41

6 Answers6

38

Angular doesn't wait for async validators to complete before firing ngSubmit. So the form may be invalid if the validators have not resolved.

Using a Subject to emit form submissions, you can switchMap to form.statusChange and filter the results.

Begin with a startWith to ensure there's no hanging emission, in the case the form is valid at the time of submission.

Filtering by PENDING waits for this status to change, and take(1) makes sure the stream is completed on the first emission after pending: VALID or INVALID.

//
// <form (ngSubmit)="formSubmitSubject$.next()">

this.formSubmitSubject$ = new Subject();

this.formSubmitSubject$
  .pipe(
    tap(() => this.form.markAsDirty()),
    switchMap(() =>
      this.form.statusChanges.pipe(
        startWith(this.form.status),
        filter(status => status !== 'PENDING'),
        take(1)
      )
    ),
    filter(status => status === 'VALID')
  )
  .subscribe(validationSuccessful => this.submitForm());

You can also add a tap that triggers the side effect of settings the form as dirty.

kyranjamie
  • 995
  • 14
  • 26
9

Use formGroup.statusChanges to wait for asyncValidators to finish before proceed to submitting form. If the asyncValidators have no error, proceed to submit. On the other hand, if it fails, don't submit. Your form should already handle failed validators. Remember to unsubscribe the subscription if you no longer need it.

 if (this.formGroup.pending) {
      let sub = this.formGroup.statusChanges.subscribe((res) => {
        if (this.formGroup.valid) {
          this.submit();
        }
        sub.unsubscribe();
      });
    } else {
      this.submit();
    }
PocoM
  • 131
  • 1
  • 2
2

markAsTouched will not fire the validation, use markAsDirty instead, then your custom validator will fire. So change...

control.markAsTouched({ onlySelf: true });

to

 control.markAsDirty({ onlySelf: true });

Also if you are using v 5, you can use the optional updateOn: 'submit', which will not update values (and therefore not validations) until form is submitted. For that, make the following changes:

this.ctrlForm = new FormGroup({
  'nome': new FormControl('', Validators.required),
  'razao_social': new FormControl('', [], CustomValidators.uniqueName),
  'cnpj': new FormControl('', CustomValidators.cnpj),
}, { updateOn: 'submit' }); // add this!

With this, it means that you do not need to call this.validateAllFormFields(control) anymore, which I assume switches some boolean flag and checks validation or something like that.

Here is a sample of a form, which always returns an error after submitting form:

https://stackblitz.com/edit/angular-rjnfbv?file=app/app.component.ts

AT82
  • 71,416
  • 24
  • 140
  • 167
  • Sorry i didn't answer before, I was busy with another project so I couldn't do more tests. I don't want to use the `updateOn: 'submit'` cause I want validation occurs on the input change. I'm thinking only at the case when the user didn't pass through a specific async validation and i want to ensure that it was executed on the submit function. I've tried to watch status change, in the case, from `PENDING` to `VALID`, but even changing from `markAsTouched` to `markAsDirty` it doesn't fire the event if call my `this.validateAllFormFields(control)` – iangoop Apr 03 '18 at 06:46
  • I'm using the async based on the directive way. I was facing the same problem, because my submit happend before my async had finish. Setting the control as dirty prevent this unacceptable submission. – davidwillianx Jun 16 '20 at 12:26
1

I just implemented a version of this in my app which manually invokes every controls synchronous and asynchronous validators and returns a boolean indicating whether or not all validation passed:

checkIfFormPassesValidation(formGroup: FormGroup) {
    const syncValidationErrors = Object.keys(formGroup.controls).map(c => {
      const control = formGroup.controls[c];
      return !control.validator ? null : control.validator(control);
    }).filter(errors => !!errors);
    return combineLatest(Object.keys(formGroup.controls).map(c => {
      const control = formGroup.controls[c];
      return !control.asyncValidator ? of(null) : control.asyncValidator(control)
    })).pipe(
      map(asyncValidationErrors => {
        const hasErrors = [...syncValidationErrors, ...asyncValidationErrors.filter(errors => !!errors)].length;
        if (hasErrors) { // ensure errors display in UI...
          Object.keys(formGroup.controls).forEach(key => {
            formGroup.controls[key].markAsTouched();
            formGroup.controls[key].updateValueAndValidity();
          })
        }
        return !hasErrors;
      })).toPromise();
  }

Usage:

onSubmitForm() {
  checkIfFormPassesValidation(this.formGroup)
    .then(valid => {
      if (valid) {
        // proceed
      }
    });
}
Stephen Paul
  • 37,253
  • 15
  • 92
  • 74
0

If I got a form (reactive form) with the class FormGroup, I use AbstractControl/Property/valid to check if the form is valid before I continue to send it to a server.

The async validator I use, must return => Promise<ValidationErrors | null> before the form gets valid again after a change to a form field. It would be weird if Google didn't design it like this... But they did!

Reactive Form Validation

ravo10
  • 895
  • 9
  • 18
0

There is also a solution implemented as a directive in this issue in angular https://github.com/angular/angular/issues/31021

ltsallas
  • 1,918
  • 1
  • 12
  • 25