$30 off During Our Annual Pro Sale. View Details »

Better Apps with Angular Reactive Forms

Better Apps with Angular Reactive Forms

In almost every Angular application, there comes a point where information is needed from the user: Creating an entry, logging in, or a simple rating mask. Angular offers a suitable form solution for every use case with Reactive Forms. But user input can become very complex even in its simplest form: Fields need to be validated, can have complex dependencies on each other and forms should be well testable. In this session, Fabian Gosebrink will address the complexity of Angular Forms and show solutions he has encountered while maintaining many projects, web applications and related forms. The session will cover practical examples, complex validations and their solutions, so that in the end forms will not be a problem in the next Angular projects.

Fabian Gosebrink

October 12, 2022
Tweet

More Decks by Fabian Gosebrink

Other Decks in Technology

Transcript

  1. Reactive Forms import { NgModule } from '@angular/core'; import {

    ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [...], imports: [ ReactiveFormsModule, ], providers: [], }) export class AppModule {} 1 2 3 4 5 6 7 8 9 10 11
  2. Reactive Forms import { NgModule } from '@angular/core'; import {

    ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [...], imports: [ ReactiveFormsModule, ], providers: [], }) export class AppModule {} 1 2 3 4 5 6 7 8 9 10 11 import { ReactiveFormsModule } from '@angular/forms'; imports: [ ReactiveFormsModule, ], import { NgModule } from '@angular/core'; 1 2 3 @NgModule({ 4 declarations: [...], 5 6 7 8 providers: [], 9 }) 10 export class AppModule {} 11
  3. FormControl @Component({ selector: 'my-app', template: `<input [formControl]="name">`, }) export class

    AppComponent { name = new FormControl(); } 1 2 3 4 5 6 7 template: `<input [formControl]="name">`, name = new FormControl(); @Component({ 1 selector: 'my-app', 2 3 }) 4 export class AppComponent { 5 6 } 7
  4. FormControl import { Component } from '@angular/core'; import { FormControl

    } from '@angular/forms' @Component({ selector: 'my-app', template: ` <input [formControl]="name"> {{ name.status | json}} {{ name.value | json}} {{ name.errors | json}} `, }) export class AppComponent { name = new FormControl(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  5. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 FormControl
  6. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 FormControl
  7. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean value: any | T 1 status: FormControlStatus 2 3 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 FormControl
  8. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean value: any | T 1 status: FormControlStatus 2 3 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pending: boolean disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 FormControl
  9. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean value: any | T 1 status: FormControlStatus 2 3 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pending: boolean disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 errors: ValidationErrors | null value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 FormControl
  10. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean value: any | T 1 status: FormControlStatus 2 3 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pending: boolean disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 errors: ValidationErrors | null value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pristine: boolean dirty: boolean touched: boolean untouched: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 9 10 11 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 FormControl
  11. value: any | T 1 status: FormControlStatus 2 valid: boolean

    3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean value: any | T 1 status: FormControlStatus 2 3 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pending: boolean disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 errors: ValidationErrors | null value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pristine: boolean dirty: boolean touched: boolean untouched: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 9 10 11 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valueChanges: Observable<...> statusChanges: Observable<...> value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 13 14 FormControl
  12. myFormGroup = new FormGroup({ firstName: new FormControl() }); import {

    Component } from '@angular/core'; 1 import { FormControl, FormGroup } from '@angular/forms' 2 3 @Component({ 4 selector: 'my-app', 5 template: ` 6 <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> 7 <input formControlName="firstName"> 8 <button>Send</button> 9 </form> 10 ` 11 }) 12 export class AppComponent { 13 14 15 16 17 onSubmit() { 18 console.log(this.myFormGroup.get("firstName")) 19 } 20 } 21
  13. myFormGroup = new FormGroup({ firstName: new FormControl() }); import {

    Component } from '@angular/core'; 1 import { FormControl, FormGroup } from '@angular/forms' 2 3 @Component({ 4 selector: 'my-app', 5 template: ` 6 <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> 7 <input formControlName="firstName"> 8 <button>Send</button> 9 </form> 10 ` 11 }) 12 export class AppComponent { 13 14 15 16 17 onSubmit() { 18 console.log(this.myFormGroup.get("firstName")) 19 } 20 } 21 <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> <input formControlName="firstName"> <button>Send</button> </form> import { Component } from '@angular/core'; 1 import { FormControl, FormGroup } from '@angular/forms' 2 3 @Component({ 4 selector: 'my-app', 5 template: ` 6 7 8 9 10 ` 11 }) 12 export class AppComponent { 13 myFormGroup = new FormGroup({ 14 firstName: new FormControl() 15 }); 16 17 onSubmit() { 18 console.log(this.myFormGroup.get("firstName")) 19 } 20 } 21
  14. value: any | T status: FormControlStatus valid: boolean invalid: boolean

    pending: boolean disabled: boolean enabled: boolean errors: ValidationErrors | null pristine: boolean dirty: boolean touched: boolean untouched: boolean valueChanges: Observable<...> statusChanges: Observable<...>
  15. FormBuilder @Component({ selector: 'my-app', template: ` <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> <input

    formControlName="firstName"> <button>Send</button> </form>` }) export class AppComponent { myFormGroup = new FormGroup({ firstName: new FormControl() }); onSubmit() { console.log(this.myFormGroup.get("firstName")) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  16. myFormGroup: FormGroup; constructor(private fb: FormBuilder){ this.myFormGroup = fb.group({ firstName: null

    }); } @Component({ 1 selector: 'my-app', 2 template: ` 3 <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> 4 <input formControlName="firstName"> 5 <button>Send</button> 6 </form>` 7 }) 8 export class AppComponent { 9 10 11 12 13 14 15 16 17 18 onSubmit(){ 19 console.log(this.myFormGroup.value) 20 } 21 } 22
  17. myFormGroup: FormGroup; constructor(private fb: FormBuilder){ this.myFormGroup = fb.group({ firstName: null

    }); } @Component({ 1 selector: 'my-app', 2 template: ` 3 <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> 4 <input formControlName="firstName"> 5 <button>Send</button> 6 </form>` 7 }) 8 export class AppComponent { 9 10 11 12 13 14 15 16 17 18 onSubmit(){ 19 console.log(this.myFormGroup.value) 20 } 21 } 22 <form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()"> <input formControlName="firstName"> <button>Send</button> </form>` @Component({ 1 selector: 'my-app', 2 template: ` 3 4 5 6 7 }) 8 export class AppComponent { 9 10 myFormGroup: FormGroup; 11 12 constructor(private fb: FormBuilder){ 13 this.myFormGroup = fb.group({ 14 firstName: null 15 }); 16 } 17 18 onSubmit(){ 19 console.log(this.myFormGroup.value) 20 } 21 } 22
  18. profileForm = new FormGroup({ firstName: new FormControl(''), lastName: new FormControl(''),

    address: new FormGroup({ street: new FormControl(''), city: new FormControl(''), state: new FormControl(''), zip: new FormControl('') }) }); 1 2 3 4 5 6 7 8 9 10 profileForm = this.fb.group({ firstName: '', lastName: '', address: this.fb.group({ street: '', city: '', state: '', zip: '' }), }); 1 2 3 4 5 6 7 8 9 10
  19. FormArray myFormGroup: FormGroup; formArray: FormArray; constructor(private fb: FormBuilder) { this.formArray

    = fb.array([new FormControl("")]) this.myFormGroup = fb.group({ myFormArray: this.formArray }); @Component({ 1 selector: "my-app", 2 template: ` 3 <form [formGroup]="myFormGroup"> 4 <div formArrayName="myFormArray"> 5 <input *ngFor="let control of formArray.controls; index as i" 6 [formControlName]="i" /> 7 </div> 8 <button>Send</button> 9 </form>` 10 }) 11 export class AppComponent { 12 13 14 15 16 17 18 19 20 } 21 } 22
  20. FormArray myFormGroup: FormGroup; formArray: FormArray; constructor(private fb: FormBuilder) { this.formArray

    = fb.array([new FormControl("")]) this.myFormGroup = fb.group({ myFormArray: this.formArray }); @Component({ 1 selector: "my-app", 2 template: ` 3 <form [formGroup]="myFormGroup"> 4 <div formArrayName="myFormArray"> 5 <input *ngFor="let control of formArray.controls; index as i" 6 [formControlName]="i" /> 7 </div> 8 <button>Send</button> 9 </form>` 10 }) 11 export class AppComponent { 12 13 14 15 16 17 18 19 20 } 21 } 22 <form [formGroup]="myFormGroup"> <div formArrayName="myFormArray"> <input *ngFor="let control of formArray.controls; index as i" [formControlName]="i" /> </div> <button>Send</button> </form>` @Component({ 1 selector: "my-app", 2 template: ` 3 4 5 6 7 8 9 10 }) 11 export class AppComponent { 12 myFormGroup: FormGroup; 13 formArray: FormArray; 14 15 constructor(private fb: FormBuilder) { 16 this.formArray = fb.array([new FormControl("")]) 17 this.myFormGroup = fb.group({ 18 myFormArray: this.formArray 19 }); 20 } 21 } 22
  21. FormArray @Component({ selector: "my-app", template: ` <form [formGroup]="myForm" (ngSubmit)="onSubmit()"> <div

    *ngFor="let formGroup of formArray.controls"> <my-comp [formGroup]="formGroup"></my-comp> </div> </form>` }) export class AppComponent { myForm: FormGroup; formArray: FormArray; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.formArray = this.formBuilder.array([new FormGroup({ ... })]); this.myForm = this.formBuilder.group({ myFormArray: this.formArray }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  22. Validation @Component({ /* ... */ }) export class FormComponent implements

    OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: '', lastName: '', age: '', room: null, } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  23. Validation @Component({ /* ... */ }) export class FormComponent implements

    OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', Validators.required], age: ['', Validators.required], room: [null, Validators.required], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  24. Validation class Validators { static min(min: number): ValidatorFn static max(max:

    number): ValidatorFn static required(control: AbstractControl): ValidationErrors | null static requiredTrue(control: AbstractControl): ValidationErrors | null static email(control: AbstractControl): ValidationErrors | null static minLength(minLength: number): ValidatorFn static maxLength(maxLength: number): ValidatorFn static pattern(pattern: string | RegExp): ValidatorFn static nullValidator(control: AbstractControl): ValidationErrors | null static compose(validators: ValidatorFn[]): ValidatorFn | null static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null } 1 2 3 4 5 6 7 8 9 10 11 12 13
  25. Custom Validation export class MyCustomValidator { static myValidator(control: AbstractControl) {

    if (control.value ... ) { return { someProp: true }; } return null; } } 1 2 3 4 5 6 7 8 9 10
  26. Custom Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', Validators.required], age: ['', Validators.required], room: [null, Validators.required], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  27. Custom Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [ Validators.required, MyCustomValidator.myValidator ] ], age: ['', Validators.required], room: [null, Validators.required], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  28. Async Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [ Validators.required, MyCustomValidator.myValidator ] ], age: ['', Validators.required], room: [null, Validators.required], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  29. Async Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [ Validators.required ], [ MyCustomValidator.myAsyncValidator ] ], age: ['', Validators.required], room: [null, Validators.required], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
  30. Cross Field Validation @Component({ /* ... */ }) export class

    FormComponent implements OnInit { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [Validators.required, ...]], age: ['', Validators.required], room: [null, Validators.required], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  31. @Component({ /* ... */ }) export class FormComponent implements OnInit

    { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [Validators.required, ...]], age: ['', Validators.required], room: [null, Validators.required], }, { validators: [...], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Cross Field Validation
  32. Cross Field Validation export class MyFormValidator { static formValidator(/* ...

    */): ValidatorFn { return (formGroup: AbstractControl) => { // return null if everything is okay // otherwise an object }; } } 1 2 3 4 5 6 7 8
  33. Cross Field Validation export class MyFormValidator { static formValidator(value: any):

    ValidatorFn { return (formGroup: AbstractControl) => { // return null if everything is okay // otherwise an object }; } } 1 2 3 4 5 6 7 8
  34. Cross Field Validation export class MyFormValidator { static formValidator(value: any):

    ValidatorFn { return (formGroup: AbstractControl) => { const control1 = formGroup.get('control1Name'); const control2 = formGroup.get('control2Name'); if(/* ... */) { // ... } return null; }; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14
  35. @Component({ /* ... */ }) export class FormComponent implements OnInit

    { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [Validators.required, ...]], age: ['', Validators.required], room: [null, Validators.required], }, { validators: [MyFormValidator.formValidator], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Cross Field Validation
  36. @Component({ /* ... */ }) export class FormComponent implements OnInit

    { myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ['', Validators.required], lastName: ['', [Validators.required, ...]], age: ['', Validators.required], room: [null, Validators.required], }, { validators: [MyFormValidator.formValidator], } ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 validators: [MyFormValidator.formValidator], @Component({ /* ... */ }) 1 export class FormComponent implements OnInit { 2 myForm: FormGroup; 3 4 constructor(private formBuilder: FormBuilder) {} 5 6 ngOnInit() { 7 this.myForm = this.formBuilder.group( 8 { 9 firstName: ['', Validators.required], 10 lastName: ['', [Validators.required, ...]], 11 age: ['', Validators.required], 12 room: [null, Validators.required], 13 }, 14 { 15 16 } 17 ); 18 } 19 } 20 Cross Field Validation
  37. validators: [MyFormValidator.formValidator(42)], @Component({ /* ... */ }) 1 export class

    FormComponent implements OnInit { 2 myForm: FormGroup; 3 4 constructor(private formBuilder: FormBuilder) {} 5 6 ngOnInit() { 7 this.myForm = this.formBuilder.group( 8 { 9 firstName: ['', Validators.required], 10 lastName: ['', [Validators.required, ...]], 11 age: ['', Validators.required], 12 room: [null, Validators.required], 13 }, 14 { 15 16 } 17 ); 18 } 19 } 20 Cross Field Validation
  38. Testing describe('MyValidator', () => { describe('should return valid if', ()

    => { it('value is empty', () => { // Arrange const formControl = new FormControl(''); // Act const result = MyCustomValidator.myValidator(formControl); // Assert expect(result.ageNotValid).toBe(true); }); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14
  39. Testing it('should not return null when given age is under

    the required age', () => { const validatorFn = RestrictAgeValidator.restrictAgeValidator(18); const formGroup = new FormGroup({ age: new FormControl(17), room: new FormControl({ text: 'room 2', value: 'room-2' }), }); const result = validatorFn(formGroup); expect(result).not.toEqual(null); }); describe('RestrictAgeValidator', () => { 1 describe('restricts age correctly', () => { 2 3 4 5 6 7 8 9 10 11 12 13 14 15 it('should return null when given age is above the required age', () => { 16 const validatorFn = RestrictAgeValidator.restrictAgeValidator(18); 17 18 const formGroup = new FormGroup({ 19 age: new FormControl(20), 20 room: new FormControl({ text: 'room 2', value: 'room-2' }), 21 }); 22 23
  40. Testing it('should not return null when given age is under

    the required age', () => { const validatorFn = RestrictAgeValidator.restrictAgeValidator(18); const formGroup = new FormGroup({ age: new FormControl(17), room: new FormControl({ text: 'room 2', value: 'room-2' }), }); const result = validatorFn(formGroup); expect(result).not.toEqual(null); }); describe('RestrictAgeValidator', () => { 1 describe('restricts age correctly', () => { 2 3 4 5 6 7 8 9 10 11 12 13 14 15 it('should return null when given age is above the required age', () => { 16 const validatorFn = RestrictAgeValidator.restrictAgeValidator(18); 17 18 const formGroup = new FormGroup({ 19 age: new FormControl(20), 20 room: new FormControl({ text: 'room 2', value: 'room-2' }), 21 }); 22 23 it('should return null when given age is above the required age', () => { const validatorFn = RestrictAgeValidator.restrictAgeValidator(18); const formGroup = new FormGroup({ age: new FormControl(20), room: new FormControl({ text: 'room 2', value: 'room-2' }), }); const result = validatorFn(formGroup); expect(result).toEqual(null); }); 5 const formGroup = new FormGroup({ 6 age: new FormControl(17), 7 room: new FormControl({ text: 'room 2', value: 'room-2' }), 8 }); 9 10 const result = validatorFn(formGroup); 11 12 expect(result).not.toEqual(null); 13 }); 14 15 16 17 18 19 20 21 22 23 24 25 26 27 }); 28 }); 29
  41. ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ..., lastName: ...,

    ... }, { ... } ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ValueChanges
  42. ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ..., lastName: ...,

    ... }, { ... } ); this.myForm.valueChanges.subscribe(console.log); this.myForm.get('firstName').valueChanges.subscribe(console.log); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ValueChanges
  43. export class ProfileComponent { profileForm = new FormGroup({ firstName: new

    FormControl('John'), lastName: new FormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue(5); } } 1 2 3 4 5 6 7 8 9 10
  44. export class ProfileComponent { profileForm = new FormGroup({ firstName: new

    FormControl('John'), lastName: new FormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue(5); } } 1 2 3 4 5 6 7 8 9 10
  45. Compiled with problems ERROR src/app/profile/profile.component.ts:20:50 - error TS2345: Argument of

    type '5' is not assignable to parameter of type 'string | null'. 20 this.profileForm.controls.firstName.setValue(5); 1 2 3 4 5 6 7 8 9
  46. export class ProfileComponent { profileForm = new FormGroup({ firstName: new

    FormControl('John'), lastName: new FormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue(5); } } 1 2 3 4 5 6 7 8 9 10
  47. export class ProfileComponent { profileForm = new FormGroup<{ firstName: FormControl<string

    | null>; lastName: FormControl<string | null>; }>({ firstName: new FormControl('John'), lastName: new FormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue('2'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13
  48. export class ProfileComponent { profileForm = new FormGroup<{ firstName: FormControl<string

    | null>; lastName: FormControl<string | null>; }>({ firstName: new FormControl(5), // DOESN'T WORK! lastName: new FormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue('2'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13
  49. Migration export class MyComponent { private control = new FormControl(42);

    private group = new FormGroup({}); private array = new FormArray([]); private fb = new FormBuilder(); } 1 2 3 4 5 6 7
  50. Migration export class MyComponent { private control = new FormControl(42);

    private group = new FormGroup({}); private array = new FormArray([]); private fb = new FormBuilder(); } 1 2 3 4 5 6 7 export class MyComponent { private control = new UntypedFormControl(42); private group = new UntypedFormGroup({}); private array = new UntypedFormArray([]); private fb = new UntypedFormBuilder(); } 1 2 3 4 5 6 7
  51. Migration export class ProfileComponent { profileForm = new FormGroup({ firstName:

    new FormControl('John'), lastName: new FormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue(5); } } 1 2 3 4 5 6 7 8 9 10
  52. Migration export class ProfileComponent { profileForm = new UntypedFormGroup({ firstName:

    new UntypedFormControl('John'), lastName: new UntypedFormControl('Doe'), }); populate() { this.profileForm.controls.firstName.setValue(5); } } 1 2 3 4 5 6 7 8 9 10
  53. Migration profileForm = new FormGroup<any>({ export class ProfileComponent { 1

    2 firstName: new UntypedFormControl('John'), 3 lastName: new UntypedFormControl('Doe'), 4 }); 5 6 populate() { 7 this.profileForm.controls.firstName.setValue(5); 8 } 9 } 10
  54. Migration @Component(/* ... */) export class FormSimpleGroupComponent implements OnInit {

    myForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.myForm = this.formBuilder.group({ firstName: '', lastName: '', age: 0, }); } onSubmit() { const formValue = this.myForm.value; console.log(formValue); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
  55. Migration @Component(/* ... */) export class FormSimpleGroupComponent implements OnInit {

    myForm: FormGroup<{ firstName: FormControl<string>; lastName: FormControl<string>; age: FormControl<number>; }>; constructor(private formBuilder: FormBuilder) {} ngOnInit() { /* ... */ } onSubmit() { /* ... */ } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
  56. Migration interface UserForm { firstName: FormControl<string>; lastName: FormControl<string>; age: FormControl<number>;

    } @Component(/* ... */) export class FormSimpleGroupComponent implements OnInit { myForm: FormGroup<UserForm>; constructor(private formBuilder: FormBuilder) {} ngOnInit() { /* ... */ } onSubmit() { /* ... */ } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
  57. Control Value Accessor import { Component } from '@angular/core'; import

    { ControlValueAccessor } from '@angular/forms'; @Component({ /* ... */ }) export class MyComponent implements ControlValueAccessor { writeValue(obj: any): void { // ... } registerOnChange(fn: any): void { // ... } registerOnTouched(fn: any): void { // ... } setDisabledState?(isDisabled: boolean): void { // ... } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  58. import { Component } from '@angular/core'; import { ControlValueAccessor }

    from '@angular/forms'; @Component({ selector: 'app-my-component', templateUrl: './my.component.html', styleUrls: ['./my.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyComponent), multi: true, }, ], }) export class MyComponent implements ControlValueAccessor { //... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Control Value Accessor
  59. import { Component } from '@angular/core'; import { ControlValueAccessor }

    from '@angular/forms'; @Component({ selector: 'app-my-component', templateUrl: './my.component.html', styleUrls: ['./my.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyComponent), multi: true, }, ], }) export class MyComponent implements ControlValueAccessor { //... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyComponent), multi: true, }, ], import { Component } from '@angular/core'; 1 import { ControlValueAccessor } from '@angular/forms'; 2 3 @Component({ 4 selector: 'app-my-component', 5 templateUrl: './my.component.html', 6 styleUrls: ['./my.component.scss'], 7 8 9 10 11 12 13 14 }) 15 export class MyComponent implements ControlValueAccessor { 16 17 //... 18 19 } 20 Control Value Accessor