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.)
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 like:
// 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.
As explained more fully in the Angular Documentation, with reactive forms, you do not bind the form directly to your model. Rather, you use a FormBuilder to build a FormGroup object (essentially, "the form") that will maintain it's own model. During construction, you have the opportunity to set initial values in the form, which you would usually do from your model.
You then bind form controls in your template to the form's model. User interaction with the form controls updates the form's model.
When you are ready to do something with the form data (e.g. "submit" the form), you can get the values from the form fields by using either the FormGroup's value property or it's getRawValue() method - these two behave differently, see the documentation for details.
Once you get the values from the form, if you wish, you can update your model with the values from the form.
You can subscribe to changes in your form group and use it to update your model. But this is not safety. Because you must ensure that your form fields match the model fields or add verification that the fields in the model exist.
bindModelToForm(model: any, form: FormGroup) {
const keys = Object.keys(form.controls);
keys.forEach(key => {
form.controls[key].valueChanges.subscribe(
(newValue) => {
model[key] = newValue;
}
)
});
}
Full code of my service:
referenceFields - means if you have complex fields like student: { name, group }
where group is a referenced model, and you need to be able to get only id from this model:
import { Injectable } from '@angular/core';
import { FormGroup } from "@angular/forms";
@Injectable({
providedIn: 'root'
})
export class FormService {
constructor() {
}
bindModelToForm(model: any, form: FormGroup, referenceFields: string[] = []) {
if (!this.checkFieldsMatching(model, form)) {
throw new Error('FormService -> bindModelToForm: Model and Form fields is not matching');
}
this.initForm(model, form);
const formKeys = Object.keys(form.controls);
formKeys.forEach(key => {
if (referenceFields.includes(key)) {
form.controls[key].valueChanges.subscribe(
(newValue) => {
model[key] = newValue.id;
}
)
} else {
form.controls[key].valueChanges.subscribe(
(newValue) => {
model[key] = newValue;
}
)
}
});
}
private initForm(model: any, form: FormGroup) {
const keys = Object.keys(form.controls);
keys.forEach(key => {
form.controls[key].setValue(model[key]);
});
}
private checkFieldsMatching(model: any, form: FormGroup): boolean {
const formKeys = Object.keys(form.controls);
const modelKeys = Object.keys(model);
formKeys.forEach(formKey => {
if (!modelKeys.includes(formKey)) {
return false;
}
});
return true;
}
}