How can I bind a form to a model in Angular 6 using reactive forms?

Don’t use [(ngModel)]! Reactive forms are much nicer. They make manual ngModel bindings obsolete, and they have some pretty sweet built-in features only a couple of which I’m going to cover in this answer.

Binding to the form

If you’re binding to a form control such as a text input, use this template syntax:

<ng-container [formGroup]="this.myFormGroup">
    <input type="text" formControlName="field1">
    <input type="text" formControlName="field2">
    <ng-container formGroupName="subgroupName">
        <input type="text" formControlName="subfield2">
    </ng-container>
    <input type="text" formControlName="myRequiredField">
</ng-container>

(field1, field2, subgroupName, subfield2, and myRequiredField are all arbitrary control and control group names that correspond to parts of your form, see below when creating the FormGroup object.)

Note on <ng-container>: You can, of course, also use any other tag in place of <ng-container> if that makes more semantic sense. E.g. <form [formGroup]="this.myFormGroup">. I used <ng-container> here because it doesn’t create an extra HTML element when rendered; <ng-container><div /></ng-container> appears in the DOM tree as just a <div/>. Great if you use CSS that depends on the tags having a certain structure.

Read-only data bindings to the model of the FormGroup are accessed a little differently in your template:

{{ this.myFormGroup.get('field1').value }}
{{ this.myFormGroup.get('subgroupName.subfield2').value }}
<!-- Hint: use an array if you have field names that contain "." -->
{{ this.myFormGroup.get(['subgroupName', 'subfield2']).value }}

Creating the FormGroup

In your component class, in constructor() (this should be before the template renders), use the following syntax to build a form group to talk to this form:

import { FormBuilder, FormGroup, Validators } from '@angular/forms';

...
    public readonly myFormGroup: FormGroup;
...
    constructor(private readonly formBuilder: FormBuilder) {
        this.myFormGroup = this.formBuilder.group({
            field1: [],
            field2: [],
            subgroupName: this.formBuilder.group({
                subfield2: [],
            }),
            myRequiredField: ['', Validators.required],
        });
        this.retrieveData();
    }

Filling your form with data

If your component needs to retrieve data from a service as it loads, you must make sure it starts the transfer after the form has been built, then use patchValue() to put the data from your object into the FormGroup:

    private retrieveData(): void {
        this.dataService.getData()
            .subscribe((res: SomeDataStructure) => {
                // Assuming res has a structure matching the template structure
                // above, e.g.:
                // res = {
                //     field1: "some-string",
                //     field2: "other-string",
                //     subgroupName: {
                //         subfield2: "another-string"
                //     },
                // }
                // Values in res that don't line up to the form structure
                // are discarded. You can also pass in your own object you
                // construct ad-hoc.
                this.myFormGroup.patchValue(res);
            });
    }

Getting data out of the form

Now, say your user clicks submit and now you need to get the data back out of your form and POST it back to your API thru a service. Just use getRawValue:

public onClickSubmit(): void {
    if (this.myFormGroup.invalid) {
        // stop here if it's invalid
        alert('Invalid input');
        return;
    }
    this.myDataService.submitUpdate(this.myFormGroup.getRawValue())
        .subscribe((): void => {
            alert('Saved!');
        });
}

All these techniques eliminate the need for any [(ngModel)] bindings, since the form maintains its own internal model inside the FormGroup object.

Leave a Comment

tech