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

User interaction with a checkbox triggers a state change in Angular

Here is the code snippet I am working with, where clicking should set the checked value to false: @Component({ template: ` <input type="checkbox" [checked]="checked" (change)="onChange()"> ` }) export class AppC ...

Direct your attention to utilizing directives when dealing with an empty ngselect

I have a form with a template driven design that includes a mandatory ng-select dropdown and a textbox. When I submit the form, certain conditions should be met: If the ng-select dropdown is empty, the focus should shift to the ng-select dropdown. If the ...

Mastering the art of utilizing Angular Material's custom-palette colors for maximum impact. Unle

I have implemented a custom material-color palette where I defined the primary and accent palettes with specific shades as shown below: $my-app-primary: mat-palette($md-lightprimary ,500,900,A700 ); $my-app-accent: mat-palette($md-lightaccent, 500,900 ...

Pull information from API and populate a dropdown menu in an Angular material select input

I am facing an issue with displaying data from an API in a mat select input field. I have tried to iterate over the data using a for loop but it is not working as expected. Can someone help me figure out how to properly populate the mat select input with d ...

Consolidating Angular 4 Observable HTTP requests into a single Observable to optimize caching

I am currently working on an Angular 4 application that serves as a dashboard for a system. Several different components within the application make calls to the same REST endpoint using identical TypeScript service classes. While this setup functions corr ...

What is the most efficient way to halt the pipe if the value of an HTML input element remains unchanged using RxJS?

I'm currently incorporating RxJS into my Angular 9 project. My goal is to bind a keyup event to an input field and trigger an HTTP request whenever the user types a new value. Here's the code snippet I have: fromEvent(this.inputBox.nativeElemen ...

Issue with Material UI grid not rendering properly in TypeScript environment

I've been trying to replicate a grid from material-ui using React and Typescript. You can see a live demo here. I modified the example to work with Typescript, so my demo.tsx file looks like this: Code goes here... If you check out the live demo, y ...

Tips for fixing the TS2345 compilation error when working with React

Attempting to implement the setState method in React has resulted in a compile error. Any solutions to this issue would be greatly appreciated. Frontend: react/typescript articleApi.tsx import axios from 'axios'; import {Article} from '../ ...

Trigger @HostListener event after a certain delay

I am currently working on implementing a basic infinite-scroll directive in Angular2. To achieve this, I am utilizing @HostListener('window:scroll') to capture the scroll event and extract the data from the $target. My concern is that every time ...

`The Art of Curved Arrows in sigjma.js, typescript, and npm`

I have encountered an issue while trying to draw curved arrows in sigma.js within my TypeScript npm project. The error occurs on the browser/client-side: Uncaught TypeError: Cannot read properties of undefined (reading 'process') at Sigma.pro ...

Troubleshooting: Angular 2 View not reflecting changes after array push

I have encountered an issue with my two child components. They are both meant to share data from a json file that I load using the http.get/subscribe method. Oddly enough, when I try to push new data into the array, it doesn't seem to update in the vi ...

Is there a way to host an AngularJS 2 application without needing to serve all the files in the `node_modules` directory as well?

Struggling to get the Angular 2 seed application up and running. Upon using npm install, a plethora of files are placed into node_modules that seem excessive for what is necessary to serve alongside the seed application code. Is there a way to only serve ...

Bring in properties from a separate file in Vue3

Currently, I am utilizing Vue3 along with the options API. Within my setup, there are various Vue components that rely on a shared prop defined as: exercise: { type: Object as PropType<Exercise>, required: true, }, To streamline this pro ...

The Art of Validating Configurations Using io-ts

I have recently switched to using io-ts instead of runtypes in a fresh project. In terms of configuration validation, my approach involves creating an object that specifies the types for each part of the config; const configTypeMap = { jwtSecret: t.str ...

Encountering NoResourceAdapterError when using @admin-bro/nestjs alongside @admin-bro/sequelize and MySQL?

Encountering a similar issue with '@admin-bro/sequelize' NoResourceAdapterError: No compatible adapters found for the provided resource import { Database, Resource } from '@admin-bro/sequelize'; import { AdminModule } from '@admin- ...

The distinctUntilChanged function encounters an issue when no comparison function is given

considering the following code: /** * Generating a stream of car objects from an array. */ from([ { name: 'Porsche', model: '911' }, { name: 'Porsche', model: '911' }, { name: 'Ferrari', model: &apo ...

How to easily switch between multiple side navigation menus using Angular 4

Below is the structure of my components: app --app.component.html --app.component.ts -dashboard --dashboard.component.html --dashboard.component.ts -sales --sales.component.html --sales.component.ts -taxreports --taxreports.c ...

When publishing, TypeScript-compiled JS files fail to be included, even though they are included during the build process in Debug and Release modes

My .NET MAUI project includes TypeScript files in the Scripts\scriptfiles.ts folder, which are compiled into wwwroot\js\scriptfiles.js. Everything functions properly until my client attempts to publish it, at which point all script files go ...

Creating intricate mazes using canvas drawing techniques

I recently developed a maze generator as a personal project utilizing a graph. While the generation logic works perfectly, I am facing challenges when it comes to rendering the maze. In my approach, each cell is represented by an array of 4 edges where the ...

Angular 4 Bootstrap 4 Collapsible Navigation Bar

Struggling for a while now trying to achieve the exact functionality I desire. Within my Angular Universal App, there is a vertical navigation bar at the top that I want to make responsive for mobile devices. I am utilizing Bootstrap 4 Alpha 6 and ngx-boot ...