0

I'm trying to force an update on the validators of all the form groups of a custom stepper just before the registration at the end of the final step, like so :

public async register(): Promise<void> {
    // do stuff

    // update validity of all forms from all steps
    const stepForms = this.stepFactories.map(step => step.form);
    stepForms.forEach(group => {
        if (group !== null && group !== undefined) {
            this.updateValueAndValidity(group);
        }
    });

    // check if any form is KO
    if (stepForms.every(form => form.valid || form.disabled)) {
        try {
            result = await this.registrationService.register(model);
        } catch (error) {
            // do stuff
        }

        // do stuff
    }
}

One of the form groups contain another formGroup with an asynchronous validator with 2 calls to the server which works as follow:

export function fooAsyncValidator(fooService: FooService, fooParams: FooParams): AsyncValidatorFn {
    return (group: AbstractControl): Observable<ValidationErrors | null> => {
        const fooControlVal1 = group.get('FooVal1').value;
        const fooControlVal2 = group.get('FooVal2').value;
    
        // do stuff

        let availableFoo: number;
        // first call to server, which is answered according to chrome debugger
        return fooService.getAvailableFoo(fooParams).pipe(
            flatMap((avFoo: number) => {
                availableFoo = avFoo;

                // second call to server which is answered after I get to check the validity
                return fooService.getConsumedFoo(fooParam, availableFoo);
            }),
            map((consumedFoo: number) => {
                // do stuff with available Foo and consumedFoo
                if (/* stuff */) {
                    return { availableFooError: true };
                }

                return null;
            }),
        );
    }
}

According to chrome debugger, when I apply the updateValueAndValidity on this form group, the first call, fooService.getAvailableFoo(...), is answered on time, but the second call, fooService.getConsumedFoo(...), is answered later. This result in the status of the form group being "PENDING".

I have found a similar subject connected to this tutorial but I don't really understand how to apply it to my current problem as I am quite new to Angular.

I found out in another part of the code that use a CdkStepper that setting the value of selectedIndex to the number of a step with the same form group would correctly update the validity of the form.

For reference, here is the code of the updateValueAndValidity custom method, which apply updateValueAndValidity on each fromGroup and formControl recursively:

private updateValueAndValidity(group: FormGroup): void {
    group.updateValueAndValidity({ emitEvent: false });
    for (const field in group.controls) { // 'field' is a string
        const control = group.get(field);
        if (control) {
            if (control instanceof FormGroup) {
                updateValueAndValidity(control);
        } else {
            if (control.disabled) {
                control.enable({ emitEvent: false });
                control.updateValueAndValidity({ emitEvent: false });
                if (!control['invalidActived'] || !control.invalid) {
                    control.disable({ emitEvent: false });
                }
            } else {
                control.updateValueAndValidity({ emitEvent: false });
            }
        }
    }
}

In summary: When I update the value and validity of a form group with 2 calls to the server, the status switch to pending as one the 2 isn't answered yet. I want to know how to wait for the pending to end.

Edit: Reworked a bit of the question layout and changed title since main stepper is a custom one, not a CdkStepper.

Edit 2: So I found this github thread but it doesn't seem to get a definitive answer before being closed...

Edit 3: Found another lead in this stackoverflow post:

  • this answer seems to be interesting, but I would need to include all four formGroups in the subscribe => not sure how to do it yet
  • the last answer hint at this github request wherein the author describe his custom solution

I just found this code in the app I'm working on, burried inside a component:

export function awaitAsyncValidator(form: FormGroup): Observable<number> {
    return timer(0, 500).pipe(
        tap((timerCount: number) => {
            if (timerCount > 200) {
                throw new Error('The form is blocked in "pending" status.');
            }
        }),
        filter(() => !form.pending),
        take(1),
    );
}

It is used with a Subscription objet attached to it in a subscribe. Currently testing it out.

Oudini
  • 23
  • 1
  • 7
  • Just a guess: try and use `concatMap` instead of `flatMap` in your `fooAsyncValidator` – lbsn Jul 23 '21 at 10:53
  • I'm aware it would be better to use `concatMap` since `flatMap` is deprecated. However, it doesn't change anything after testing this solution =/ and sadly my work is currently limited to bug fixing not refactoring code. Still thank you. – Oudini Jul 23 '21 at 12:44
  • As for your Edit 2, the "definitive" answer from that thead seems to be in this [comment](https://github.com/angular/angular/issues/13200#issuecomment-371440273) : be sure your observable always completes. – lbsn Jul 26 '21 at 08:10
  • @Ibsn, I tried to understand this comment and how to apply it to my case but that doesn't seems to be it yet =( However I have found some other leads I'm adding in Eddit 3 right now. – Oudini Jul 27 '21 at 07:33

1 Answers1

0

Finally found the correct way to update the validity of all my forms.
The following function allows to wait for the pending status to end.

export function awaitAsyncValidator(form: FormGroup): Observable<number> {
    return timer(0, 500).pipe(
        tap((timerCount: number) => {
            if (timerCount > 200) {
                throw new Error('The form is blocked in "pending" status.');
            }
        }),
        filter(() => !form.pending),
        take(1),
    );
}

Then using awaitAsyncValidator().subscribe(...); instead of await awaitAsyncValidator();:

public async register(): Promise<void> {
    // do stuff

    // update validity of all forms from all steps
    const stepForms: FormGroup[] = this.stepFactories.map(step => step.form);
    const globalForm = new FormGroup({});
    for (let [index,group] of stepForms.entries()) {
        if (!isNullOrUndefined(group)) {
            globalForm.addControl(index.toString(), group);
        }
    }
    updateValueAndValidity(globalForm);
    awaitAsyncValidator(globalForm).subscribe(
        async () => {
            // check if any form is KO
            if (globalForm.valid || globalForm.disabled) {
                try {
                    result = await this.registrationService.register(model);
                } catch (error) {
                    // do stuff
                }

                // do stuff
            }
        });
}

Thanks @Ibsn for your help.

Oudini
  • 23
  • 1
  • 7