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

3

38

Prior to angular 6, I was using [(ngModel)] to directly bind my form field to the model. This is now deprecated (can't use with reactive forms) and I am not sure how to update my model with the form values. I could use form.getRawValue() but this would require me to replace my current model with the new rawValue - this is not favourable since my main model is larger and has more fields than the local form model.

Any ideas?

Chaves answered 14/7, 2018 at 19:37 Comment(7)
You could assign each column from form to your object. Is it convinient?Condonation
there is no ngModel deprecation mentioned in angular change log (github.com/angular/angular/blob/master/…) or at angular.io (angular.io/guide/forms#two-way-data-binding-with-ngmodel)Damnable
@Damnable Sorry, I meant to say I get a warning if I use ngModel with reactive forms.Chaves
@MaximKasyanov Thanks, I am currently considering doing this but looking for other options as well.Chaves
please post (if you can) reactive form code using ngModel which stopped working since ng6Damnable
Did you even read Angular's tutorial explicilty explaining how to do this ?Footman
@Damnable angular.io/api/forms/FormControlName#use-with-ngmodelHalimeda
M
59

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.

Mirilla answered 5/12, 2018 at 22:52 Comment(6)
"this.myFormGroup.patchValue(res);" this was the key for me, the documentation sucks - I was trying to figure out how to load an Edit form from an object received from a service call (I'm new to Angular) - thanks!Colligate
Where is the reactive form getting bound to a Model object?Festination
It's done thru the [formGroup]="this.myFormGroup" directive. This binds it to the model stored in the FormGroup object, which you can later get at by calling this.myFormGroup.getRawValue, etc.Mirilla
very helpful to see specific examples. Hard to find !Balch
This answer helped me to understand form builder better. Thanks a ton.Adenoidal
If we already have FormControl defined, we can create formGroup like this : ` this.myFormGroup = formBuilder.group({}) this.myFormGroup.addControl('field1', this.field1FormControl) this.myFormGroup.addControl('field2', this.field2FormControl)`Herder
N
28

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.

Note answered 14/7, 2018 at 22:24 Comment(8)
> You then bind form controls in your template to the form's model. User interaction with the form controls updates the form's model. Are you suggesting to use [(ngModel)] within the template?Weksler
No. You create a FormGroup in the component, and bind the HTML inputs to that with the attribute 'formControlName'. Again, see the documentation, e.g.angular.io/api/forms/FormControlNameNote
How do you bind reactive form to model's id? using hidden form control?Cyma
You don't. That's kind of the point. The form has it's own model. It's not bound to yours. You can get the 'value' property of the form, and do what you want with it, including updating your model, but your model is explicitly NOT bound to the form.Note
Was looking for this info. Thanks.Haden
You can keep saying see the doc's but the doc's don't show you how to do this. They don't show you how to BIND your reactive form to a Model object. They show how to log to the console some data but couldn't find any example for how to bind one way and two way to the backing model. I started with the link from the warning message then yours regarding with were a similar story. No actually explanation or examples of BINDING to a MODEL. That is what ngModel does... it binds with reactive forms I have form controls with data but no data getting to a model.Festination
This is the first clear explanation of how this all fits together that I have found, after checking out many many blog posts.Balch
@RodneyS.Foley I suggest you re-read this response. You'll then understand that Reactive Forms are not designed to behave the same way as Template Drive Forms. What ngModel does do not exists with Reactive Forms. There is no "binding to my (business) model". You are in charge of filling your business model with the values from the form model. If you don't like that, then use a Template Driven form.Phytopathology
I
4

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;
    }
}
Issi answered 24/11, 2019 at 12:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.