What is the process for connecting a form to a model in Angular 6 using reactive forms?

In the time before angular 6, my approach involved utilizing [(ngModel)] to establish a direct binding between my form field and the model. However, this method is now considered deprecated (unusable with reactive forms) and I find myself at a loss on how to update my model with the values from the form. One option could be to utilize form.getRawValue(), but this would necessitate replacing my current model with the new rawValue - a less than ideal solution considering my main model contains more fields than the local form model.

Does anyone have any suggestions or ideas on how to tackle this issue?

Answer №1

Avoid using the [(ngModel)] directive! Reactive forms offer a more elegant solution. They render manual ngModel bindings unnecessary, and come with several useful built-in features, a few of which I'll explain below.

Connecting to the form

When connecting to a form control like a text input, utilize 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 arbitrary names corresponding to elements in your form, as explained when creating the FormGroup object.)

Note on <ng-container>: Instead of <ng-container>, you can use any other tag that fits better semantically. For instance,

<form [formGroup]="this.myFormGroup">
. I opted for <ng-container> here because it doesn't introduce an extra HTML element upon rendering;
<ng-container><div /></ng-container>
displays as just a <div/> in the DOM tree. This is beneficial if your CSS relies on specific tag structures.

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

{{ this.myFormGroup.get('field1').value }}
{{ this.myFormGroup.get('subgroupName.subfield2').value }}
<!-- Tip: Use an array for field names containing "." -->
{{ this.myFormGroup.get(['subgroupName', 'subfield2']).value }}

Setting up the FormGroup

In your component class, within the constructor() (before the template is rendered), build a form group to link to your form using this syntax:

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();
    }

Populating your form with data

If your component requires data retrieval from a service during initialization, ensure that data transfer begins after the form is constructed, then use patchValue() to insert the data from your object into the FormGroup:

    private retrieveData(): void {
        this.dataService.getData()
            .subscribe((res: SomeDataStructure) => {
                // Assuming res matches the template structure
                // e.g.:
                // res = {
                //     field1: "some-string",
                //     field2: "other-string",
                //     subgroupName: {
                //         subfield2: "another-string"
                //     },
                // }
                // Values in res not aligning with the form structure
                // are disregarded. You can also pass your own object.
                this.myFormGroup.patchValue(res);
            });
    }

Retrieving data from the form

After the user submits the form and you need to extract the data to send back to your API via a service, use getRawValue:

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

By employing these methods, there's no need for [(ngModel)] bindings, as the form maintains its internal model within the FormGroup object.

Answer №2

Explained in depth in the Angular Documentation, when using reactive forms, the form is not directly bound to your model. Instead, a FormGroup object (referred to as "the form") is created with a FormBuilder that maintains its own model. During setup, initial values can be set in the form, usually sourced from your model.

Form controls in the template are then linked to the form's model. Any user interaction with these controls will update the form's model.

When it comes time to process the form data (for example, submitting the form), the values from the form fields can be retrieved using either the value property of the FormGroup or its getRawValue() method - note that their behavior differs, refer to the documentation for specifics.

After obtaining the form values, if desired, your model can be updated with the information collected from the form.

Answer №3

To keep your model updated with changes in your form group, you can subscribe to those changes. However, it is important to note that this method is not foolproof. You must ensure that your form fields match the corresponding model fields or implement a verification process to confirm their existence.

bindModelToForm(model: any, form: FormGroup) {
    const keys = Object.keys(form.controls);
    keys.forEach(key => {
        form.controls[key].valueChanges.subscribe(
            (newValue) => {
                model[key] = newValue;
            }
        )
    });
}

If you have complex fields within your form like student: { name, group }, where group is a referenced model and you only need its ID, consider using the referenceFields parameter:

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 do not match');
        }
        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;
    }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having an issue with the `map` operator in Angular 8 due to using RxJS version 6.4

After updating my app to Angular 8 and installing the new version of rxjs ~6.4.0, I encountered an issue with my .map function. The error message in VC reads: "Property 'map' does not exist on type 'Observable'." I've attempted to ...

Using async method in controller with NestJS Interceptor

I am seeking a way to capture both the result and any potential errors from an asynchronous method within a controller using an interceptor. When an error is thrown, the interceptor can respond accordingly. However, I am struggling to figure out how to tri ...

Eslint is unable to locate file paths when it comes to Solid.js tsx extensions

Whenever I attempt to import TSX components (without the extension), Eslint raises an error stating that it's unable to resolve the path: However, if I add the TSX extension, it then prompts me to remove it: This represents my configuration in .esli ...

TS1056 Error: Accessors can only be utilized with ECMAScript 5 or newer versions targeted

I'm currently working on a SharePoint framework project that has the following folder layout: One of the directories is named dataaccess and contains a webpart factory method. The file Sharepointdataprovider.ts includes this code snippet: import { ...

Guide on How to Embed an Angular Module in a Next.js (React App)

I'm currently in the process of integrating Micro Front End. As part of this integration, we need to include an Angular app in Next.js. To do so, we have injected the angular remoteEntry.js as shown below: injectScript({ global: 'app', ...

Implementing Custom Font Awesome Icons in Your Angular Project

I recently upgraded to a fontawesome subscription with a paid plan and have successfully created some custom icons. Now, I'm looking to integrate these icons into my angular app. Here are the dependencies listed in my package.json file: "@fortawe ...

Error encountered with the PrimeNG Angular2 Accordion component

I am currently utilizing the PrimeNG accordion module. After importing all components successfully, I encountered an issue with a newly created component. Despite verifying that all modules were imported correctly, I continue to receive the error message ...

Solving Angular Circular Dependencies

My popupservice allows me to easily open popup components: export class PopupService { alert() { this.matdialog.open(PopupAlertComponent); } yesno() { this.matdialog.open(PopupYesNoComponent); } custom() { this.matdialog.open(PopupCustomCompon ...

Apply a unique CSS class to the first two elements, then skip the following two elements, and continue this pattern using ngFor in Angular and flex styling

Here is a code snippet that displays a list of products using *ngFor in Angular: <div class="container-products"> <div class="item-product" *ngFor="let product of ['product A', 'product B', 'prod ...

Include a conditional statement in ng keypress

When a user types a specific value into a text input, I want to display a specific div. This is what my template looks like: <input type="text" id="jobTitle" (click)="autoCompleteClick()" (keypress)="autoCompleteKeypress()" name="autocomplete" place ...

Having difficulty overriding the Content-type in http.put while using Angular RC2

I'm encountering an issue in the latest version of rc2 that didn't exist in older versions and I'm unsure if it's a bug or something I'm doing wrong. The problem arises when making a http.put request where I need to set Content-ty ...

What is the method for extracting children from a singular object in json-server rather than an array?

I am currently utilizing json-server as a mock-backend to fetch child data from a single object. The main table is called sentinel and the secondary table is named sensor It can be observed that sensors is an array, while sentinel is an object. I have ...

The NgbTooltip fails to display the arrow icon ( ▼ ) within the NgbPopover's window

There appears to be an issue with the arrow div not functioning properly within NgpPopover body due to conflicting arrow classes for NgbPopover's div.arrow and NgbTooltip's div.arrow. This seems to be a known problem in ng-bootstrap when using bo ...

What is the reason behind the lag caused by setTimeout() in my application, while RxJS timer().subscribe(...) does not have the same

I am currently working on a component that "lazy loads" some comments every 100ms. However, I noticed that when I use setTimeout for this task, the performance of my application suffers significantly. Here is a snippet from the component: <div *ngFor ...

To enhance VS IntelliSense and type checking in react-intl's FormattedMessage component, assign an id that aligns with a custom TypeScript interface

Due to the limitations of react-localization in terms of date and number formats, as well as its heavy reliance on a single developer, our team made the decision to transition to react-intl for a more stable long-term solution. Check out the contributors ...

Angular Obsidian Theme

Hey there! I've successfully implemented a toggle button that switches my Ionic 3 app to dark mode. However, I'm unsure about where exactly I should define the global class [class.dark-theme]="dark". It's essential for this class to be in th ...

Tips for outputting data in TypeScript when a search form is updated

Below is the structure of my HTML file: <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type"" content="text/html; charset=utf-8"/> </head> <body> <input ...

Why is the bearing attribute important in determining the location of an object?

I have started using this plugin to enable GPS functionality in my app. If you are interested, the link to the plugin can be found here: https://github.com/mauron85/cordova-plugin-background-geolocation My question is about the bearing property of the lo ...

Encountered a Webpack issue when trying to load the primeng.min

I recently initiated a fresh project using yo aspnetcore-spa. My goal is to integrate the PrimeNG component library. Upon installing font-awesome and primeng: npm install font-awesome primeng --save I included CSS in the webpack vendor list: vendor: [ ...

Different ways to categorize elements of Timeline using typescript

I have some code that generates a timeline view of different stages and their corresponding steps based on an array of stages. Each stage includes its name, step, and status. My goal is to organize these stages by name and then display the steps grouped un ...