<!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
}
}