<!DOCTYPE html>

<html>
  <head>
    <script src="./lib/main.ts"></script>
  </head>

  <body>
    <my-app>loading...
    </my-app>
  </body>
</html>
//our root app component
import {  Component, NgModule, VERSION } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { ExampleComponent } from './example.component'
 
@Component({
  selector: 'my-app',
  template: `
    <div>
      <p>
        This example demonstrates that the form state is not being updated after an async validation finished
      </p>
      <p>
      Type something validates based on whether it contains the string '123' or not</p>
      <p>The radio button toggles the additional - required - question</p>
      <p><strong>Open the console while you toggle Yes/No</strong></p>
    </div>
    <app-example></app-example>
  `,
})
export class App {
  name: string;
  constructor() {
    this.name = `Angular! v${VERSION.full}`;
  }
}

@NgModule({
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    ],
  declarations: [App, ExampleComponent],
  bootstrap: [App],
})
export class AppModule {}
// Shim the environment
import 'core-js/client/shim';

// Angular requires Zones to be pre-configured in the environment
import 'zone.js/dist/zone';

//main entry point
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app';

import './style.css';

platformBrowserDynamic().bootstrapModule(AppModule);
h1,
p {
  font-family: sans-serif;
}
import { Component, OnDestroy, } from '@angular/core';
import { FormBuilder, Validators, AbstractControl, ValidationErrors, FormGroup, FormControl } from '@angular/forms';
  
import { Subscription, of, BehaviorSubject, Observable, timer, throwError } from 'rxjs';
import { delay, tap, switchMap, catchError } from 'rxjs/operators';
 
@Component({
    selector: 'app-example',
    template: `<div *ngIf="data$ | async" style="background: lightgray; padding: 20px;font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">
    <form [formGroup]="myForm" class="row">
        <p>
            <strong>Type something *</strong>
            <br>
            <input type="text" formControlName="asyncTest" />
        </p>
        <p >
            <strong>Add a question? *</strong>
            <br>
            <input type="radio" id="yes" name="addQuestion"  formControlName="addQuestion" value="yes">
            <label for="yes">Yes</label><br>
            <input type="radio" id="no"  name="addQuestion" formControlName="addQuestion" value="no">
            <label for="no">No</label><br>
        </p>
        <p *ngIf="myForm.get('additionalText')">
            <strong>Type something *</strong>
            <br>
            <input type="text" formControlName="additionalText" />
        </p>
    </form>

    valid: {{ myForm.valid }}
    invalid: {{ myForm.invalid }}
    pending: {{ myForm.pending }}
    <br>
    {{ myForm.value | json }}
</div>`
})
export class ExampleComponent implements OnDestroy {
    // @see https://github.com/angular/angular/issues/14542
    // @see https://github.com/angular/angular/issues/20424
    // @see https://github.com/angular/angular/pull/20806
    private statusSubscription: Subscription;
    public myForm = new FormGroup({});
    private startExecution = new BehaviorSubject<string>(this.myForm.value);
    public data$ = this.startExecution.asObservable().pipe(
        tap((formValue) => {
            this.statusSubscription?.unsubscribe();

            this.myForm = this.getForm(formValue);

            this.statusSubscription = this.myForm.statusChanges
                .subscribe((state) => {
                    console.log('new state: ', state)
                });

            console.log('state is: ', this.myForm.pending ? 'pending' : this.myForm.valid ? 'valid': 'not valid');

            this.myForm.valueChanges.subscribe(() => this.startExecution.next(this.myForm.value))
        }),
    );

    constructor(
        // private fb: FormBuilder
        ) {}

    /**
     * Unsubscribe from the statusChanges
     */
    public ngOnDestroy(): void {
        this.statusSubscription?.unsubscribe();
    }

    /**
     * Construction of the form
     * @param formValue
     */
    public getForm(formValue) {
        const form = new FormGroup({});

        // define controls
        const control1 = new FormControl(formValue.asyncTest || '123', {
            validators: [Validators.required],
            asyncValidators: this.getAsyncValidators()
        });

        const control2 = new FormControl(formValue.addQuestion || 'no', {
            validators: [Validators.required],
        });

        const control3 = new FormControl(formValue.additionalText || '', {
            validators: [Validators.required],
        });

        // add controls
        form.addControl('asyncTest', control1);
        form.addControl('addQuestion', control2);

        // conditionally add control
        if(formValue.addQuestion === 'yes') {
            form.addControl('additionalText', control3);
        }

        return form;
    }

    /**
     * Example of a async validator
     * faked here
     * usually this would be a http request
     */
    private getAsyncValidators() {
        // return []; // test for sync case
        return (control: AbstractControl): Observable<ValidationErrors | null> => {

            console.log('starting validation');

            return timer(600).pipe(
                delay(2000),
                switchMap(() => {
                    // this would be a http call, dummy for demonstration purposes
                    if (control.value === '123'){
                    return throwError({});
                    }
                    return of(null);
                }),
                catchError((error) => {
                    // handle error
                    return of(null);
                }),
                tap(() => console.log('async validator finished')),
                tap(() => console.log('but there is no change in form state')),
            );
        };
    }
}
 

{
  "name": "@plnkr/example-angular-form-bug",
  "version": "0.0.0",
  "description": "Example of formbug in angular",
  "dependencies": {
    "@angular/common": "~10.0.0",
    "@angular/compiler": "~10.0.0",
    "@angular/core": "~10.0.0",
    "@angular/platform-browser": "~10.0.0",
    "@angular/platform-browser-dynamic": "~10.0.0",
    "@angular/forms": "~10.0.0",
    "core-js": "2.6.11",
    "rxjs": "^6.6.0",
    "tslib": "^2.0.0",
    "zone.js": "~0.10.3"
  },
  "main": "./lib/main.ts",
  "plnkr": {
    "runtime": "system",
    "useHotReload": true
  }
}
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}