Patching and updating data
Angular SPA Typescript Visual Studio Code

How to Create Nested Reactive Forms in an Angular Application

Welcome to today’s post.

In today’s post I will be explaining what a nested Angular Reactive form is, how to implement a nested form, and how to assign and update data within a nested form.

In a previous post I showed how to set and update data within a basic Angular Reactive form. In that post I explained how to use two fundamental Reactive Forms API methods, setValue() and patchValue() to initialise and update data within form controls.

In this post, I will go one step further and show how to initialize and update data within form controls that are within nested form groups.

Explaining the Basic Reactive Form

I will start with a Reactive form that is not nested.

Each Reactive Form that is instantiated in our component requires form controls to be instantiated within a FormBuilder group object.

The instance variable and form controls of our Reactive form are then referenced within the HTML template. 

Below show how the Reactive Form is instantiated within the component source.

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-demo-nested-reactive-form',
  templateUrl: './demo-nested-reactive-form.component.html',
  styleUrls: ['./demo-nested-reactive-form.component.scss']
})
export class DemoNestedReactiveFormComponent implements OnInit {
  memberProfileForm: any;

  constructor(private fb: FormBuilder) { 
    this.initForm(fb);
  }

  private initForm(fb: FormBuilder) {  
    this.memberProfileForm = fb.group({      
        firstName: new FormControl(''),
        lastName: new FormControl(''),
        dateJoined: new FormControl(''), 
        libraryName: new FormControl(''),
        libraryLocation: new FormControl(''),
        status: new FormControl(''),
        street: new FormControl(''),
        suburb: new FormControl(''),
        state: new FormControl(''),
        postcode: new FormControl(''),
        email: new FormControl(''),
        phone: new FormControl(''),
        mobile: new FormControl(''),
        numberCurrentLoans: new FormControl(''),
        numberReturns: new FormControl(''), 
        numberOverdue: new FormControl(''),
        lastLoanDate: new FormControl(''),
        lastReturnDate: new FormControl('')      
    })
  }
}

Below is the HTML template that declares a form that displays membership details, address, contact details, and loan summary sections.

<p>Nested Reactive Form</p>

<mat-card>
    <mat-card-title>
        <h2>Member Profile Form</h2>
    </mat-card-title>
    
    <mat-card-content>
        <button type="button" class="btn btn-primary" id="btnBack" 
          (click)="eventBack($event.value)">Back to Home
        </button>
    </mat-card-content>
  
    <mat-card-content>
        <div id="heading" class="heading"><strong>Member Profile</strong></div>
    </mat-card-content>
  
    <form [formGroup]="memberProfileForm">

    <mat-card-content>
        <div class="form-group">
            <label>First Name:</label>
            <input readonly formControlName="firstName" name="firstName" 
                class="form-control col-sm-6 input-sm" 
                placeholder="First Name" 
                placement="right" ngbTooltip="First Name">
        </div>

        <div class="form-group">
            <label>Last Name:</label>
            <input readonly formControlName="lastName" name="lastName" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Last Name" 
                placement="right" ngbTooltip="Last Name">
        </div>

        <div class="form-group">
            <label>Date Joined:</label>
            <input readonly formControlName="dateJoined" name="dateJoined" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Date Joined" 
                placement="right" ngbTooltip="Date Joined">
        </div>

        <div class="form-group">
            <label>Library Name:</label>
            <input readonly formControlName="libraryName" 
                name="libraryName" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Library Name" 
                placement="right" ngbTooltip="Library Name">
        </div>

        <div class="form-group">
            <label>Library Location:</label>
            <input readonly formControlName="libraryLocation" 
                name="libraryLocation" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Library Location" 
                placement="right" ngbTooltip="Library Location">
        </div>

        <div class="form-group">
            <label>Status:</label>
            <input readonly formControlName="status" name="status" 
                class="form-control col-sm-6 input-sm" placeholder="Status" 
                placement="right" ngbTooltip="Status">
        </div>
    </mat-card-content>

    <mat-card-content>
        <div id="address" class="heading"><strong>Address</strong></div>
    </mat-card-content>

    <mat-card-content>
        <div class="form-group">
            <label>Street:</label>
            <input readonly formControlName="street" name="street" 
                class="form-control col-sm-6 input-sm" placeholder="Street" 
                placement="right" ngbTooltip="Street">
        </div>

        <div class="form-group">
            <label>Suburb:</label>
            <input readonly formControlName="suburb" name="suburb" 
                class="form-control col-sm-6 input-sm" placeholder="Suburb" 
                placement="right" ngbTooltip="Suburb">
        </div>

        <div class="form-group">
            <label>State:</label>
            <input readonly formControlName="state" name="state" 
                class="form-control col-sm-6 input-sm" placeholder="State" 
                placement="right" ngbTooltip="State">
        </div>

        <div class="form-group">
            <label>Postcode:</label>
            <input readonly formControlName="postcode" name="postcode" 
                class="form-control col-sm-6 input-sm" placeholder="postcode" 
                placement="right" ngbTooltip="Postcode">
        </div>
    </mat-card-content>

    <mat-card-content>
        <div id="contact" class="heading"><strong>Contact Details</strong></div>
    </mat-card-content>

    <mat-card-content>
        <div class="form-group">
            <label>Email:</label>
            <input readonly formControlName="email" name="email" 
                class="form-control col-sm-6 input-sm" placeholder="Email" 
                placement="right" ngbTooltip="Email">
        </div>

        <div class="form-group">
            <label>Phone:</label>
            <input readonly formControlName="phone" name="phone" 
                class="form-control col-sm-6 input-sm" placeholder="Phone" 
                placement="right" ngbTooltip="Phone">
        </div>

        <div class="form-group">
            <label>Mobile:</label>
            <input readonly formControlName="mobile" name="mobile" 
                class="form-control col-sm-6 input-sm" placeholder="Mobile" 
                placement="right" ngbTooltip="Mobile">
        </div>
    </mat-card-content>

    <mat-card-content>
        <div id="loanSummary" class="heading"><strong>Loan Summary</strong></div>
    </mat-card-content>

    <mat-card-content>
        <div class="form-group">
            <label>Number Current Loans:</label>
            <input readonly formControlName="numberCurrentLoans" 
                name="numberCurrentLoans" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Number Current Loans" 
                placement="right" ngbTooltip="Number Current Loans">
        </div>

        <div class="form-group">
            <label>Number Returns:</label>
            <input readonly formControlName="numberReturns" 
                name="numberReturns" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Number Returns" 
                placement="right" ngbTooltip="Number Returns">
        </div>

        <div class="form-group">
            <label>Number Overdue:</label>
            <input readonly formControlName="numberOverdue" 
                name="numberOverdue" class="form-control col-sm-6 input-sm" 
                placeholder="Number Overdue" 
                placement="right" ngbTooltip="Number Overdue">
        </div>

        <div class="form-group">
            <label>Last Loan Date:</label>
            <input readonly formControlName="lastLoanDate" 
                name="lastLoanDate" class="form-control col-sm-6 input-sm" 
                placeholder="Last Loan Date" 
                placement="right" ngbTooltip="Last Loan Date">
        </div>

        <div class="form-group">
            <label>Last Return Date:</label>
            <input readonly formControlName="lastReturnDate" 
                name="lastLoanDate" class="form-control col-sm-6 input-sm" 
                placeholder="Last Return Date" placement="right" 
                ngbTooltip="Last Return Date">
        </div>

    </mat-card-content>

    </form>

    <mat-card-actions>
        <div class="example-button-row">
            <button type="button" class="btn btn-primary" id="btnRead" 
                (click)="eventReadData($event.value)">
                Read Data
            </button>
        </div>
        <div class="example-button-row">
            <button type="button" class="btn btn-primary" 
                id="btnUpdate" (click)="patchFormValues($event.value)">
                Update Data
            </button>
        </div>
        <div class="example-button-row">
            <button type="button" class="btn btn-primary" id="btnUpdate2" 
                (click)="patchFormObjectValues($event.value)">
                Update Object Data
            </button>
        </div>
    </mat-card-actions>
</mat-card>

Notice that the above HTML form definition includes a heading that separates each set of form controls visually:

<div id="[section name]" class="heading"><strong>[section heading]</strong></div>

From an application component perspective, the above does not separate the groups logically. To be able to separate the form controls into logical groups, we will need to nest each of the groups:

memberDetails

address

contactDetails

loanSummary

In the next section, I will show how to separate the form controls into nested groups in the component source and in the component HTML template.

Explaining the Nested Reactive Form

With the above logical groupings, we can choose to have the fields that are part of member details in its own group or have them as the set of header fields in the form interface.  I will show how to leave some fields as heading fields and move other fields into nested groupings.

What I will show is the groupings and fields within each group.

memberDetails

        firstName

        lastName

        dateJoined

        libraryName

        libraryLocation

        status

address

        street

        suburb

        state

        postcode

contactDetails

        email

        phone

        mobile

loanSummary

        numberCurrentLoans

        numberReturns

        numberOverdue

        lastLoanDate

        lastReturnDate  

With nested form controls, the instantiation of the form controls requires then to be within a form group instance. This requires a FormGroup instance to be created, then FormControl instances created within the form group. An example is below:

form_group_1: new FormGroup({
    form_control_1: new FormControl(''),
    …
    form_control_N: new FormControl(''),
}),

Our basic form controls in their nested groupings are declared as shown:

private initForm(fb: FormBuilder) {
    this.memberProfileForm = fb.group({      
      firstName: new FormControl(''),
      lastName: new FormControl(''),
      dateJoined: new FormControl(''), 
      libraryName: new FormControl(''),
      libraryLocation: new FormControl(''),
      status: new FormControl(''),
      address: new FormGroup({
        street: new FormControl(''),
        suburb: new FormControl(''),
        state: new FormControl(''),
        postcode: new FormControl(''),
      }),
      contactDetails: new FormGroup({
        email: new FormControl(''),
        phone: new FormControl(''),
        mobile: new FormControl(''),
      }),
      loanSummary: new FormGroup({
        numberCurrentLoans: new FormControl(''),
        numberReturns: new FormControl(''), 
        numberOverdue: new FormControl(''),
        lastLoanDate: new FormControl(''),
        lastReturnDate: new FormControl('')      
      })
    })
}

To associate each nested form control to their form group (the parent), the HTML template will require the attribute formGroupName to be declared with a value set to the nested form group name, within a div element. To associate the street field with the address form group, we do this as shown:

<div class="form-group" formGroupName="address">
    <label>Street:</label>
    <input readonly formControlName="street" name="street" 
        class="form-control col-sm-6 input-sm" placeholder="Street" 
        placement="right" ngbTooltip="Street">
</div>

To associate the form control value in the HTML template to the corresponding from control variable within the component code, we are required to make us of the formControlName attribute within the input element. The value of the formControlName element must coincide with the name of the FormControl instance declared under the FormGroup parent instance. For the street form control, this is done as shown:

formControlName="street"

To leave our member details fields un-nested as header fields, we can leave the formGroupName out (as we did for the base form definition). An example of how this is done is shown below:

<div class="form-group">
    <label>First Name:</label>
    <input readonly formControlName="firstName" name="firstName" 
        class="form-control col-sm-6 input-sm" placeholder="First Name" 
        placement="right" ngbTooltip="First Name">
</div>

With the inclusion of the nested form grouping associations, our HTML template looks as follows:

<p>Nested Reactive Form</p>

<mat-card>
    <mat-card-title>
        <h2>Member Profile Form</h2>
    </mat-card-title>
    
    <mat-card-content>
        <button type="button" class="btn btn-primary" id="btnBack" 
            (click)="eventBack($event.value)">Back to Home
        </button>
    </mat-card-content>
  
    <mat-card-content>
        <div id="heading" class="heading"><strong>
            Member Profile</strong>
        </div>
    </mat-card-content>
  
    <form [formGroup]="memberProfileForm">

    <mat-card-content>
        <div class="form-group">
            <label>First Name:</label>
            <input readonly formControlName="firstName" name="firstName" 
                class="form-control col-sm-6 input-sm" 
                placeholder="First Name" 
                placement="right" ngbTooltip="First Name">
        </div>

        <div class="form-group">
            <label>Last Name:</label>
            <input readonly formControlName="lastName" name="lastName" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Last Name" 
                placement="right" ngbTooltip="Last Name">
        </div>

        <div class="form-group">
            <label>Date Joined:</label>
            <input readonly formControlName="dateJoined" 
                name="dateJoined" class="form-control col-sm-6 input-sm" 
                placeholder="Date Joined" 
                placement="right" ngbTooltip="Date Joined">
        </div>

        <div class="form-group">
            <label>Library Name:</label>
            <input readonly formControlName="libraryName" 
                name="libraryName" class="form-control col-sm-6 input-sm" 
                placeholder="Library Name" 
                placement="right" ngbTooltip="Library Name">
        </div>

        <div class="form-group">
            <label>Library Location:</label>
            <input readonly formControlName="libraryLocation" 
                name="libraryLocation" class="form-control col-sm-6 input-sm" 
                placeholder="Library Location" placement="right" 
                ngbTooltip="Library Location">
        </div>

        <div class="form-group">
            <label>Status:</label>
            <input readonly formControlName="status" name="status" 
                class="form-control col-sm-6 input-sm" placeholder="Status" 
                placement="right" ngbTooltip="Status">
        </div>
    </mat-card-content>

    <mat-card-content>
        <div id="address" class="heading"><strong>Address</strong></div>
    </mat-card-content>

    <mat-card-content>
        <div class="form-group" formGroupName="address">
            <label>Street:</label>
            <input readonly formControlName="street" name="street" 
                class="form-control col-sm-6 input-sm" placeholder="Street" 
                placement="right" ngbTooltip="Street">
        </div>

        <div class="form-group" formGroupName="address">
            <label>Suburb:</label>
            <input readonly formControlName="suburb" name="suburb" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Suburb" 
                placement="right" ngbTooltip="Suburb">
        </div>

        <div class="form-group" formGroupName="address">
            <label>State:</label>
            <input readonly formControlName="state" name="state" 
                class="form-control col-sm-6 input-sm" placeholder="State" 
                placement="right" ngbTooltip="State">
        </div>

        <div class="form-group" formGroupName="address">
            <label>Postcode:</label>
            <input readonly formControlName="postcode" name="postcode" 
                class="form-control col-sm-6 input-sm" 
                placeholder="postcode" 
                placement="right" ngbTooltip="Postcode">
        </div>
    </mat-card-content>

    <mat-card-content>
        <div id="contact" class="heading"><strong>
            Contact Details</strong>
        </div>
    </mat-card-content>

    <mat-card-content>
        <div class="form-group" formGroupName="contactDetails">
            <label>Email:</label>
            <input readonly formControlName="email" name="email" 
                class="form-control col-sm-6 input-sm" placeholder="Email" 
                placement="right" ngbTooltip="Email">
        </div>

        <div class="form-group" formGroupName="contactDetails">
            <label>Phone:</label>
            <input readonly formControlName="phone" name="phone" 
                class="form-control col-sm-6 input-sm" placeholder="Phone" 
                placement="right" ngbTooltip="Phone">
        </div>

        <div class="form-group" formGroupName="contactDetails">
            <label>Mobile:</label>
            <input readonly formControlName="mobile" name="mobile" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Mobile" 
                placement="right" ngbTooltip="Mobile">
        </div>
    </mat-card-content>

    <mat-card-content>
        <div id="loanSummary" class="heading"><strong>
            Loan Summary</strong>
        </div>
    </mat-card-content>

    <mat-card-content>
        <div class="form-group" formGroupName="loanSummary">
            <label>Number Current Loans:</label>
            <input readonly formControlName="numberCurrentLoans" 
                name="numberCurrentLoans" 
                class="form-control col-sm-6 input-sm" 
                placeholder="Number Current Loans" 
                placement="right" ngbTooltip="Number Current Loans">
        </div>

        <div class="form-group" formGroupName="loanSummary">
            <label>Number Returns:</label>
            <input readonly formControlName="numberReturns" 
                name="numberReturns" class="form-control col-sm-6 input-sm" 
                placeholder="Number Returns" 
                placement="right" ngbTooltip="Number Returns">
        </div>

        <div class="form-group" formGroupName="loanSummary">
            <label>Number Overdue:</label>
            <input readonly formControlName="numberOverdue" 
                name="numberOverdue" class="form-control col-sm-6 input-sm" 
                placeholder="Number Overdue" 
                placement="right" ngbTooltip="Number Overdue">
        </div>

        <div class="form-group" formGroupName="loanSummary">
            <label>Last Loan Date:</label>
            <input readonly formControlName="lastLoanDate" 
                name="lastLoanDate" class="form-control col-sm-6 input-sm" 
                placeholder="Last Loan Date" 
                placement="right" ngbTooltip="Last Loan Date">
        </div>

        <div class="form-group" formGroupName="loanSummary">
            <label>Last Return Date:</label>
            <input readonly formControlName="lastReturnDate" 
                name="lastLoanDate" class="form-control col-sm-6 input-sm" 
                placeholder="Last Return Date" 
                placement="right" ngbTooltip="Last Return Date">
        </div>
    </mat-card-content>

    </form>

    <mat-card-actions>
        <div class="example-button-row">
            <button type="button" class="btn btn-primary" id="btnRead" 
                (click)="eventReadData($event.value)">
                Read Data
            </button>
        </div>
        <div class="example-button-row">
            <button type="button" class="btn btn-primary" id="btnUpdate" 
                (click)="patchFormValues($event.value)">
                Update Data
            </button>
        </div>
        <div class="example-button-row">
            <button type="button" class="btn btn-primary" id="btnUpdate2" 
                (click)="patchFormObjectValues($event.value)">
                Update Object Data
            </button>
        </div>
    </mat-card-actions>
</mat-card>

Initializing Data within a Nested Reactive Form

To initialize data within a nested Reactive Form, we make us of the setValue() Reactive Forms API method. We pass as a parameter, a JSON object value that represents the structure of the entire nested form control structure. Inside the ngOnInit() event, we set the initial values as shown:

ngOnInit(): void {
    this.memberProfileForm.setValue(
    {
      firstName: 'Joe',
      lastName: 'Bloggs',
      dateJoined: '2022-06-01', 
      libraryName: 'Sydney',
      libraryLocation: 'Sydney',
      status: 'Active',
      address: {
        street: '1000 George Street',
        suburb: 'Sydney',
        state: 'NSW',
        postcode: '2000',
      },
      contactDetails: {
        email: 'joeb@aaaaaaa.com',
        phone: '123456789',
        mobile: '04123456789',
      },
      loanSummary: {
        numberCurrentLoans: '1',
        numberReturns: '5', 
        numberOverdue: '0',
        lastLoanDate: '2023-04-15',
        lastReturnDate: '2023-04-29' 
      }
    })
}

When the form is refreshed, the controls will look as shown:

Updating Data within a Nested Reactive Form

In addition to initializing nested data within the form, we can also selectively update sections of the values within form controls within the nested control structure. The ability to update data in particular form controls is important in forms where we receive inputs that conditionally update data that changes more often that the base header data within the form. This is where the use of the patchValue() Reactive Forms API method becomes useful.

The value passed into the method is a JSON object whose elements must correspond to existing field control names and declared under a parent form group element. An example is shown below:

patchFormValues(event: any)
{
    this.memberProfileForm.patchValue(
    {
        address: {
          street: '1001 George Street'
        },
        contactDetails: {
          phone: '1234512345'
        },
        loanSummary: {
          numberCurrentLoans: '2',
          lastLoanDate: '2023-05-15'
        }
    })
}

When the above is executed on the initial form data, the resulting output is shown:

The fields indicated with arrows are those that have been updated.

Patching Nested Forms Data with Typed Objects

Another variation of the above patching technique is when we pass in a typed object into the form via a constructor or one of its methods. This is common when we pass objects into our nested form and the data is from a data source that respects the structure of the data within our nested form.

We can represent the data within our nested form structure, we can use typed class structures that represent the data within our header form fields, form groups, and all form fields within the form groups.

This can be done with TypeScript classes that are exported as shown:

export class MemberProfile
{
  public firstName!: string;
  public lastName!: string;
  public dateJoined!: string;
  public libraryName!: string;
  public libraryLocation!: string;
  public status!: string;
  public address!: address;
  public contactDetails!: contactDetails;
  public loanSummary!: loanSummary;
}

export class address {
  public street!: string;
  public suburb!: string;
  public state!: string;
  public postcode!: string;
};

export class contactDetails {
  public email!: string;
  public phone!: string;
  public mobile!: string;
};

export class loanSummary {
  public numberCurrentLoans!: string;
  public numberReturns!: string;
  public numberOverdue!: string;
  public lastLoanDate!: string;
  public lastReturnDate!: string; 
};

An example of how we can use the typed class structure is shown below:

patchFormObjectValues(event: any)
{
    let value: MemberProfile = 
    {
        firstName: 'Jim',
        lastName: 'Bean',
        dateJoined: '2022-03-01', 
        libraryName: 'Sydney',
        libraryLocation: 'Sydney',
        status: 'Active',
        address: {
          street: '500 Pitt Street',
          suburb: 'Sydney',
          state: 'NSW',
          postcode: '2000',
        },
        contactDetails: {
          email: 'jimb@aaaaaaa.com',
          phone: '9876556789',
          mobile: '0412311111',
        },
        loanSummary: {
          numberCurrentLoans: '1',
          numberReturns: '3', 
          numberOverdue: '0',
          lastLoanDate: '2023-03-01',
          lastReturnDate: '2023-03-15' 
        }
    };

    this.memberProfileForm.patchValue(value);
}

The above has shown how to implement an Angular Reactive nested form from a basic Reactive form. I have also shown how to initialize and update data within the nested Reactive form. We now have a basis in which to make our forms more flexible with nested form controls and components.

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial