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

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. Better Apps
    with Reactive
    Forms

    View Slide

  2. Fabian
    Gosebrink
    https://offering.solutions
    @FabianGosebrink

    View Slide

  3. Template Driven
    Forms

    View Slide

  4. Template Driven
    Forms
    Reactive Forms

    View Slide

  5. Reactive Forms

    View Slide

  6. Why?

    View Slide

  7. 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

    View Slide

  8. 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

    View Slide

  9. Abstract Control

    View Slide

  10. FormControl
    FormGroup
    FormArray

    View Slide

  11. FormControl

    View Slide

  12. FormControl
    @Component({
    selector: 'my-app',
    template: ``,
    })
    export class AppComponent {
    name = new FormControl();
    }
    1
    2
    3
    4
    5
    6
    7

    View Slide

  13. FormControl
    @Component({
    selector: 'my-app',
    template: ``,
    })
    export class AppComponent {
    name = new FormControl();
    }
    1
    2
    3
    4
    5
    6
    7
    template: ``,
    name = new FormControl();
    @Component({
    1
    selector: 'my-app',
    2
    3
    })
    4
    export class AppComponent {
    5
    6
    }
    7

    View Slide

  14. FormControl
    import { Component } from '@angular/core';
    import { FormControl } from '@angular/forms'
    @Component({
    selector: 'my-app',
    template: `

    {{ 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

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. FormControl
    FormGroup
    FormArray

    View Slide

  23. FormGroup

    View Slide

  24. FormGroup

    View Slide

  25. FormGroup
    Container for
    FormControls
    FormGroups
    FormArrays

    View Slide

  26. FormGroup
    [Object object]
    {
    ...
    }

    View Slide

  27. 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

    7

    8
    Send
    9

    10
    `
    11
    })
    12
    export class AppComponent {
    13
    14
    15
    16
    17
    onSubmit() {
    18
    console.log(this.myFormGroup.get("firstName"))
    19
    }
    20
    }
    21

    View Slide

  28. 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

    7

    8
    Send
    9

    10
    `
    11
    })
    12
    export class AppComponent {
    13
    14
    15
    16
    17
    onSubmit() {
    18
    console.log(this.myFormGroup.get("firstName"))
    19
    }
    20
    }
    21


    Send

    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

    View Slide

  29. registerControl()
    addControl()
    removeControl()
    setControl()

    View Slide

  30. setValue()
    patchValue()
    reset()

    View Slide

  31. getRawValue()

    View Slide

  32. formGroup.getRawValue()

    View Slide

  33. formGroup.getRawValue()
    {
    "firstName" : "Obi Wan",
    "lastName" : "Kenobi"
    }
    1
    2
    3
    4

    View Slide

  34. formGroup.value

    View Slide

  35. formGroup.value
    {
    "firstName" : "Obi Wan"
    }
    1
    2
    3

    View Slide

  36. 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<...>

    View Slide


  37. Submit

    View Slide


  38. Submit

    Also enabled in
    pending state!

    View Slide


  39. Submit

    View Slide


  40. Submit

    Only enabled when
    valid

    View Slide

  41. FormBuilder

    View Slide

  42. FormBuilder
    @Component({
    selector: 'my-app',
    template: `


    Send
    `
    })
    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

    View Slide

  43. myFormGroup: FormGroup;
    constructor(private fb: FormBuilder){
    this.myFormGroup = fb.group({
    firstName: null
    });
    }
    @Component({
    1
    selector: 'my-app',
    2
    template: `
    3

    4

    5
    Send
    6
    `
    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

    View Slide

  44. myFormGroup: FormGroup;
    constructor(private fb: FormBuilder){
    this.myFormGroup = fb.group({
    firstName: null
    });
    }
    @Component({
    1
    selector: 'my-app',
    2
    template: `
    3

    4

    5
    Send
    6
    `
    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


    Send
    `
    @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

    View Slide

  45. 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

    View Slide

  46. FormControl
    FormGroup
    FormArray

    View Slide

  47. FormControl
    FormGroup
    FormArray

    View Slide

  48. FormArray
    Array of Form controls

    View Slide

  49. FormArray
    Array of Form controls
    Dynamic controls

    View Slide

  50. FormArray
    Array of Form controls
    Iterating over controls
    Dynamic controls

    View Slide

  51. FormArray
    Array of Form controls
    Iterating over controls
    Dynamic controls
    Serialized as array

    View Slide

  52. 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

    4

    5
    6
    [formControlName]="i" />
    7

    8
    Send
    9
    `
    10
    })
    11
    export class AppComponent {
    12
    13
    14
    15
    16
    17
    18
    19
    20
    }
    21
    }
    22

    View Slide

  53. 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

    4

    5
    6
    [formControlName]="i" />
    7

    8
    Send
    9
    `
    10
    })
    11
    export class AppComponent {
    12
    13
    14
    15
    16
    17
    18
    19
    20
    }
    21
    }
    22


    [formControlName]="i" />

    Send
    `
    @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

    View Slide

  54. FormArray
    @Component({
    selector: "my-app",
    template: `




    `
    })
    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

    View Slide

  55. Validation

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

  58. Validation
    class Validators {
    static required(control: AbstractControl): ValidationErrors | null
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    View Slide

  59. 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

    View Slide

  60. Validation
    myForm.get('controlName').errors?.min
    .max
    .required
    .requiredTrue
    .email
    .minLength
    .maxLength
    .pattern
    .nullValidator
    .compose
    .composeAsync
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    View Slide

  61. Validation
    .ng-valid
    .ng-invalid
    .ng-pending
    .ng-pristine
    .ng-dirty
    .ng-untouched
    .ng-touched
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    View Slide

  62. Custom Validation

    View Slide

  63. 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

    View Slide

  64. 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

    View Slide

  65. 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

    View Slide

  66. Async Validation

    View Slide

  67. Async Validation

    View Slide

  68. Async Validation
    export class MyCustomValidator {
    static myAsyncValidator(control: AbstractControl)
    : Observable<...> {
    // ...
    }
    }
    1
    2
    3
    4
    5
    6
    7

    View Slide

  69. 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

    View Slide

  70. 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

    View Slide

  71. Cross Field
    Validation

    View Slide

  72. 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

    View Slide

  73. @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

    View Slide

  74. 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

    View Slide

  75. 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

    View Slide

  76. 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

    View Slide

  77. @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

    View Slide

  78. @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

    View Slide

  79. 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

    View Slide

  80. Testing

    View Slide

  81. 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

    View Slide

  82. 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

    View Slide

  83. 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

    View Slide

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

    View Slide

  85. 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

    View Slide

  86. Typed Forms

    View Slide

  87. View Slide

  88. 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

    View Slide

  89. 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

    View Slide

  90. 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

    View Slide

  91. 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

    View Slide

  92. export class ProfileComponent {
    profileForm = new FormGroup<{
    firstName: FormControl;
    lastName: FormControl;
    }>({
    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

    View Slide

  93. export class ProfileComponent {
    profileForm = new FormGroup<{
    firstName: FormControl;
    lastName: FormControl;
    }>({
    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

    View Slide

  94. Migration
    ng update @angular/core
    1

    View Slide

  95. Migration
    ng update @angular/core
    1
    ng update @angular/core --migrate-only migration-v14-typed-forms
    1

    View Slide

  96. 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

    View Slide

  97. 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

    View Slide

  98. 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

    View Slide

  99. 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

    View Slide

  100. Migration
    profileForm = new FormGroup({
    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

    View Slide

  101. Migration

    View Slide

  102. 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

    View Slide

  103. Migration
    @Component(/* ... */)
    export class FormSimpleGroupComponent implements OnInit {
    myForm: FormGroup<{
    firstName: FormControl;
    lastName: FormControl;
    age: FormControl;
    }>;
    constructor(private formBuilder: FormBuilder) {}
    ngOnInit() {
    /* ... */
    }
    onSubmit() {
    /* ... */
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    View Slide

  104. Migration
    interface UserForm {
    firstName: FormControl;
    lastName: FormControl;
    age: FormControl;
    }
    @Component(/* ... */)
    export class FormSimpleGroupComponent implements OnInit {
    myForm: FormGroup;
    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

    View Slide

  105. Custom Form
    Controls

    View Slide

  106. Custom Form Controls





    1
    2
    3
    4
    5

    View Slide

  107. Control Value
    Accessor

    View Slide

  108. 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

    View Slide

  109. 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

    View Slide

  110. 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

    View Slide

  111. Recap
    FormGroup vs FormControl vs FormArray
    FormBuilder
    Validation
    Testing
    ValueChanges (rich API)
    Custom Form Controls

    View Slide

  112. Stay safe!

    View Slide