Async validation is triggered multiple times on form.setValues()

Viewed 2455

I'm using Angular 5, reactive forms approach.

I have a form inside an Angular Material dialog that I am using for both entering and editing data. My constructor looks like this:

constructor(public formBuilder: FormBuilder,
    public documentService: DocumentService, 
    @Inject(MAT_DIALOG_DATA) public data: any) {
    this.createForm();
    if (this.data.edit) {
        this.setValues();
    }
    this.setTitles();
}

The createForm call creates the reactive form where I have the async validation:

createForm() {
    this.documentForm = this.formBuilder.group({
        ...
        documentNumber: new FormControl('',
                {
                    updateOn: 'blur',
                    validators: [Validators.required],
                    asyncValidators: [this.checkDocumentNumber.bind(this)]
                }),

        ...
    });
}

If the dialog is in 'edit' mode, the setValues call patches the data that needs to be edited:

setValue() {
    this.form.patchData({
        ...
        documentNumber: this.data.document.documentNumber
        ...
    });
}

The setTitles call sets the dialog title.

The checkDocumentNumber call gets a boolean value from the server:

checkDocumentNumber(control: AbstractControl): Observable<ValidationErrors | null> {
    const formValue = this.form.value;
    return this.documentService
        .checkDocumentNumber(new Document(this.data.edit ? this.data.document.id : 0,
            form.documentNumber)).pipe(map((response: boolean) => {
            return response ? { inUse: true } : null;
        }));
}

The API call made is:

checkDocumentNumber(doc: Document) {
    return this.http.post(`/Documents/Inbox/CheckDocumentNumber`, doc);
}

The form dialog in 'edit' mode is called like this:

this.dialogService.open(DocumentDialogComponent,
    {
        data: {
            edit: true,
            document: this.document
        }
    });

The issue I'm having is that when I open the dialog to edit the document data, 9 API calls are made to check the document number. The first 5 are cancelled, then one that returns 200, another one that is cancelled and finally two more that return 200.

The same scenario in a different part of the app gets me 3 cancelled and two 200 calls.

How do I stop angular from making these unnecessary API calls? Before Angular 5, the updateOn flag wasn't there so I thought that with that out, this won't be happening.

Here's a screenshot of the API calls being made:

API calls being made

1 Answers

Angular performs the request on every value change. The docs specify async validators only if the sync ones pass "to avoid potentially expensive async validation processes (such as an HTTP request)"

Here's what I think is happening:

  • updateOn: 'blur' is for user interaction. It does not apply when you change the value via code.

  • You instantiate the form -> the validator is fired, triggering a request.

  • You patchData() in setValue() -> the validator is triggered again.

  • I'm guessing Angular internally uses switchMap for the validation Observable, which would explain the cancellations if you changed the value

I don't know how you get to 9 requests, for that I would need to see the full code (@EstusFlask as commented, http://stackoverflow.com/help/mcve).

What might help:

  • Add some sync validators. Validators.required will avoid a request for empty values, including the initial ''

  • Instead of separately patching the default value after creating the form, calculate the default for creation.
    Not needed with required in this case where default is empty, but needed in other cases, and I find it cleaner.

    const defaultDocumentNumber = this.data.edit ? this.data.document.documentNumber : '';
    ...
    documentNumber: new FormControl(defaultDocumentNumber, ... 
    
  • I'm guessing you have other controls in the form that are triggering validation. That might explain how you get to 9 calls.
    If that's the case, you could avoid some validation triggers using FormControl.patchValue()'s second parameter options, specifically the emitEvent and onlySelf fields.
    Again, can't really say without a reproducible example. Have a look at the docs.

Related