Building Complex Forms in Angular with Nested Form Groups

    Forms are an essential component of any application that requires user input. In Angular, the FormBuilder API makes it easy to create forms and handle user input. However, there are times when a form can become quite complex, especially when it contains numerous fields with interdependent values. In such cases, you'll require an effective way to organize and manage the form data.

    In this article, we’ll explore how to build complex forms in Angular using nested form groups. We’ll start by discussing the basics of Angular Forms, understand how form controls and form groups work, and then dive deeper into the concept of nested form groups.

    Understanding Angular Forms

    Angular Forms allow users to input data into the application. Forms can be reactive, template-driven, or a combination of both. Reactive forms use the RxJS library to handle form input and validation. On the other hand, template-driven forms rely on directives to handle the form input.

    Angular forms are built using form controls and form groups. Form controls lie at the heart of Angular forms and encapsulate the form input data. They are used to track the value and validation status of individual form inputs. Form groups, on the other hand, organize form controls into units that correspond to a portion of the form.

    Form Control vs. Form Group

    Form controls in Angular form the foundational building blocks of forms. Each control represents a single value, such as text, number, or date, that the user can input into the form. Hence, you typically use the FormControl class to create a form control. For example:

    import { Component } from '@angular/core';
    import { FormControl } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
        <input type="text" [formControl]="nameControl" />
      `
    })
    export class AppComponent {
      nameControl = new FormControl('');
    }
    
    

    In the code above, we've created a form control using the FormControl class. We've defined its initial value to be an empty string.

    Form groups, as the name suggests, allow you to group related form controls together. If you have a form with several inputs that correspond to a single entity, say a person, you can group them together using form groups. You typically use the FormGroup class to create a form group. For example:

    import { Component } from '@angular/core';
    import { FormControl, FormGroup } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      template: `
        <form [formGroup]="personForm">
          <input type="text" formControlName="firstName" />
          <input type="text" formControlName="lastName" />
        </form>
      `
    })
    export class AppComponent {
      personForm = new FormGroup({
        firstName: new FormControl(''),
        lastName: new FormControl('')
      });
    }
    
    

    Here, we have a form with two inputs, first name and last name. We've grouped these two form controls within a form group named personForm.

    Nested Form Groups

    The concept of nested form groups arises when you have a complex form with related data in multiple levels of hierarchy. In such cases, you can use nested form groups to manage the data more efficiently.

    Let's consider an example. Imagine you're building an application for an airline company. As part of this application, you need to create a flight booking form that captures multiple data points related to the flight and the booking. The form needs to have the following fields:

    • Origin and destination of the flight
    • Departure and arrival dates and times
    • Number of passengers
    • Passenger details for each passenger

    To build such a form, we'll need to create nested form groups. Let's take a look:

    import { Component } from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    
    @Component({
      selector: 'app-form',
      template: `
        <form [formGroup]="flightForm" (ngSubmit)="onSubmit()">
          <div formGroupName="flightDetails">
            <input type="text" formControlName="origin" />
            <input type="text" formControlName="destination" />
            
            <div formGroupName="departure">
              <input type="datetime-local" formControlName="dateTime" />
            </div>
    
            <div formGroupName="arrival">
              <input type="datetime-local" formControlName="dateTime" />
            </div>
          </div>
    
          <div formGroupName="passengers">
            <input type="number" formControlName="count" />
    
            <div *ngFor="let passenger of passengers.controls; let i=index">
              <div formGroupName="{{i}}">
                <input type="text" formControlName="firstName" />
                <input type="text" formControlName="lastName" />
                <input type="date" formControlName="dateOfBirth" />
              </div>
            </div>
          </div>
    
          <button type="submit">Book Flight</button>
        </form>
      `
    })
    export class FlightFormComponent {
      flightForm: FormGroup;
    
      constructor(private fb: FormBuilder) {}
    
      ngOnInit() {
        this.flightForm = this.fb.group({
          flightDetails: this.fb.group({
            origin: ['', Validators.required],
            destination: ['', Validators.required],
            departure: this.fb.group({
              date: ['', Validators.required],
              time: ['', Validators.required]
            }),
            arrival: this.fb.group({
              date: ['', Validators.required],
              time: ['', Validators.required]
            })
          }),
          passengers: this.fb.group({
            count: '',
            details: this.fb.array([
              this.fb.group({
                firstName: '',
                lastName: '',
                dateOfBirth: ''
              })
            ])
          })
        });
      }
    
      get passengers() {
        return this.flightForm.get('passengers.details') as FormArray;
      }
    
      onSubmit() {
        // save the form data
      }
    }
    
    

    In the code above, we've defined a flight booking form. Notice how we've grouped the different form controls using nested form groups. We've grouped the flight details and the passenger details into separate form groups, and the passenger details have an additional nested form array because we need to capture details of multiple passengers.

    When it comes to validation, Angular makes it easy to validate the form data. You can apply validators to individual form controls or form groups, and even create your custom validators when necessary. Here is an example:

    import { Component } from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    
    @Component({
      selector: 'app-form',
      template: `
        <form [formGroup]="flightForm">
          <div formGroupName="passengers">
            <div *ngFor="let passenger of passengers.controls; let i=index">
              <div formGroupName="{{i}}">
                <input type="text" formControlName="firstName" required minlength="2" />
                <input type="text" formControlName="lastName" required minlength="2" />
                <input type="date" formControlName="dateOfBirth" max="{{ maxDate }}" />
              </div>
            </div>
          </div>
          <button type="submit">Book Flight</button>
        </form>
      `
    })
    export class FlightFormComponent {
      flightForm: FormGroup;
      maxDate = new Date().toISOString().split('T')[0];
    
      constructor(private fb: FormBuilder) {}
    
      ngOnInit() {
        this.flightForm = this.fb.group({
          passengers: this.fb.group({
            details: this.fb.array([
              this.fb.group({
                firstName: ['', Validators.minLength(2)],
                lastName: ['', Validators.minLength(2)],
                dateOfBirth: ['', Validators.max(this.maxDate)]
              })
            ])
          })
        });
      }
    
      get passengers() {
        return this.flightForm.get('passengers.details') as FormArray;
      }
    
      onSubmit() {
        // save the form data
      }
    }
    
    

    Here, we've added validation rules to the passenger details form group. We've required the first name and last name to have at least two characters, and the date of birth must be before the current date. We've achieved this using the required, minlength, and max validators provided by Angular. This is just one example of how you can apply validation in your forms.

    Conclusion

    Angular Forms provide a powerful mechanism to capture user input and validate user input. With the help of the FormBuilder API, you can create complex forms that are easy to manage and maintain. In this article, we’ve seen how to build complex forms in Angular using nested form groups. Nested form groups are ideal for capturing data with multiple levels of hierarchy. Additionally, we've seen how to apply validation to form controls and form groups.

    With these concepts, you'll be able to utilize Angular Forms to create complex forms with ease. If you have any comments or questions, feel free to share them in the comments section below.

    © 2023 Designed & Developed by José Matos.