Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developing forms and validation with Typed Angu...

Developing forms and validation with Typed Angular Reactive Forms

In almost every Angular application, you come to the point where you need information from the user. Angular Forms are a great solution for this. Angular Forms makes it possible to get rich information about the people interacting with the page and provides many possibilities with a large variety of forms.
But user inputs even in it's obviously the simplest form can get very complex: Fields must be validated, can have complex dependencies on each other and should be testable.
In this talk, Fabian Gosebrink will look at the complexity of Angular Forms and provide solutions which he encountered after maintaining lots of projects, web apps and connected forms. The talk will look at the worst examples, complex validations and the best solutions. Hopefully in your next projects, getting your user information will be easy, well testable and easy to implement.

Fabian Gosebrink

October 22, 2023
Tweet

More Decks by Fabian Gosebrink

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  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 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
  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 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
  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 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
  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 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
  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 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
  10. 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
  11. 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
  12. 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<...>
  13. 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
  14. private readonly fb = inject(FormBuilder); myFormGroup: FormGroup; ngOnInit(){ this.myFormGroup =

    this.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 implements OnInit { 9 10 11 12 13 14 15 16 17 18 19 onSubmit(){ 20 console.log(this.myFormGroup.value) 21 } 22 } 23
  15. private readonly fb = inject(FormBuilder); myFormGroup: FormGroup; ngOnInit(){ this.myFormGroup =

    this.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 implements OnInit { 9 10 11 12 13 14 15 16 17 18 19 onSubmit(){ 20 console.log(this.myFormGroup.value) 21 } 22 } 23 <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 implements OnInit { 9 private readonly fb = inject(FormBuilder); 10 11 myFormGroup: FormGroup; 12 13 ngOnInit(){ 14 this.myFormGroup = this.fb.group({ 15 firstName: null 16 }); 17 } 18 19 onSubmit(){ 20 console.log(this.myFormGroup.value) 21 } 22 } 23
  16. 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
  17. FormArray private readonly fb = inject(FormBuilder); myFormGroup: FormGroup; formArray: FormArray;

    ngOnInit() { this.formArray = this.fb.array([new FormControl("")]) this.myFormGroup = this.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 implements OnInit { 12 13 14 15 16 17 18 19 20 21 22 23 } 24
  18. FormArray private readonly fb = inject(FormBuilder); myFormGroup: FormGroup; formArray: FormArray;

    ngOnInit() { this.formArray = this.fb.array([new FormControl("")]) this.myFormGroup = this.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 implements OnInit { 12 13 14 15 16 17 18 19 20 21 22 23 } 24 <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 implements OnInit { 12 private readonly fb = inject(FormBuilder); 13 14 myFormGroup: FormGroup; 15 formArray: FormArray; 16 17 ngOnInit() { 18 this.formArray = this.fb.array([new FormControl("")]) 19 this.myFormGroup = this.fb.group({ 20 myFormArray: this.formArray 21 }); 22 } 23 } 24
  19. 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 implements OnInit { private readonly fb = inject(FormBuilder); myForm: FormGroup; formArray: FormArray; ngOnInit() { this.formArray = this.fb.array([new FormGroup({ ... })]); this.myForm = this.fb.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
  20. Validation @Component({ /* ... */ }) export class FormComponent implements

    OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  21. Validation @Component({ /* ... */ }) export class FormComponent implements

    OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  22. 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
  23. 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
  24. Custom Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  25. Custom Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  26. Async Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  27. Async Validation @Component({ /* ... */ }) export class FormComponent

    implements OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  28. Cross Field Validation @Component({ /* ... */ }) export class

    FormComponent implements OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  29. @Component({ /* ... */ }) export class FormComponent implements OnInit

    { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  30. 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
  31. 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
  32. 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
  33. @Component({ /* ... */ }) export class FormComponent implements OnInit

    { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  34. @Component({ /* ... */ }) export class FormComponent implements OnInit

    { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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 private readonly formBuilder = inject(FormBuilder); 3 4 myForm: FormGroup; 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
  35. validators: [MyFormValidator.formValidator(42)], @Component({ /* ... */ }) 1 export class

    FormComponent implements OnInit { 2 private readonly formBuilder = inject(FormBuilder); 3 4 myForm: FormGroup; 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
  36. 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
  37. 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
  38. 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
  39. ngOnInit() { this.myForm = this.formBuilder.group( { firstName: ..., lastName: ...,

    ... }, { ... } ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ValueChanges
  40. 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
  41. 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
  42. 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
  43. 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
  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. 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
  46. 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
  47. @Component(/* ... */) export class FormSimpleGroupComponent implements OnInit { private

    readonly formBuilder = inject(FormBuilder); myForm: FormGroup; 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
  48. @Component(/* ... */) export class FormSimpleGroupComponent implements OnInit { private

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

    @Component(/* ... */) export class FormSimpleGroupComponent implements OnInit { private readonly formBuilder = inject(FormBuilder); myForm: FormGroup<UserForm>; ngOnInit() { /* ... */ } onSubmit() { /* ... */ } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
  50. 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
  51. 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
  52. 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