Published:

Angular 10 - Dynamic Reactive Forms Example

Built with Angular 10.1.2

Other versions available:

This is a quick example of how to build a dynamic form with validation in Angular 10 using Reactive Forms. The example form allows selecting the number of tickets to purchase and then entering the name and email of the person each ticket is for, both fields are required and the email field must contain a valid email address.

I've setup the form to validate on submit instead of on field change, this is implemented with a submitted property in the app component that is set to true when the form is submitted for the first time, and reset to false if the reset or clear button is clicked.

The "Buy Tickets" button simply displays the form values in an alert popup if the form is valid. The "Reset" button resets the form back to it's initial state including the removal of all ticket name & email fields. The "Clear" button clears the values of ticket name & email fields but leaves the number of tickets selected.

Styling of the example is all done with Bootstrap 4.5 CSS, for more info see https://getbootstrap.com/docs/4.5/getting-started/introduction/.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-10-dynamic-reactive-forms-example)


Dynamic Reactive Forms App Component

The app component defines the form fields and validators for the dynamic form using an Angular FormBuilder to create an instance of a FormGroup that is stored in the dynamicForm property. The dynamicForm is then bound to the <form> element in the app template below using the [formGroup] directive.

The dynamic form FormGroup contains two form controls:

  • numberOfTickets is an Angular FormControl that stores the number of tickets selected. It is bound to the select input in the app component template with the directive formControlName="numberOfTickets".
  • tickets is an Angular FormArray used to hold an array of form groups (FormGroup) for storing ticket holder details. Each ticket form group contains two child form controls, one for the name and one for the email of the ticket holder.

The f and t getters are convenience properties to make it easier to access form controls from the template. So for example you can access the numberOfTickets field in the template using f.numberOfTickets instead of dynamicForm.controls.numberOfTickets.

The ticketFormGroups getter is used by the template to render the formgroup for each ticket, this getter is required when using AOT compilation because the template requires a strongly typed FormGroup object to bind with the [formGroup] directive.

The onChangeTickets() method dynamically adds or removes ticket forms from the tickets form array when the number of tickets selected is increased or decreased.

The onSubmit() method sets the submitted property to true to show validation messages, checks if the form is valid and displays the form values in an alert popup if it is.

The onReset() method resets the submitted property to false to hide validation messages, clears all form values with this.dynamicForm.reset(), and removes ticket name & email fields with this.t.clear().

The onClear() method resets the submitted property to false to hide validation messages, and clears the values of ticket name & email fields with this.t.reset().

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

@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent implements OnInit {
    dynamicForm: FormGroup;
    submitted = false;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.dynamicForm = this.formBuilder.group({
            numberOfTickets: ['', Validators.required],
            tickets: new FormArray([])
        });
    }

    // convenience getters for easy access to form fields
    get f() { return this.dynamicForm.controls; }
    get t() { return this.f.tickets as FormArray; }
    get ticketFormGroups() { return this.t.controls as FormGroup[]; }

    onChangeTickets(e) {
        const numberOfTickets = e.target.value || 0;
        if (this.t.length < numberOfTickets) {
            for (let i = this.t.length; i < numberOfTickets; i++) {
                this.t.push(this.formBuilder.group({
                    name: ['', Validators.required],
                    email: ['', [Validators.required, Validators.email]]
                }));
            }
        } else {
            for (let i = this.t.length; i >= numberOfTickets; i--) {
                this.t.removeAt(i);
            }
        }
    }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.dynamicForm.invalid) {
            return;
        }

        // display form values on success
        alert('SUCCESS!! :-)\n\n' + JSON.stringify(this.dynamicForm.value, null, 4));
    }

    onReset() {
        // reset whole form back to initial state
        this.submitted = false;
        this.dynamicForm.reset();
        this.t.clear();
    }

    onClear() {
        // clear errors and reset ticket fields
        this.submitted = false;
        this.t.reset();
    }
}


Dynamic Reactive Forms App Component Template

The app component template contains the html and angular template syntax for displaying the example dynamic form in your browser. The form element uses the [formGroup] directive to bind to the dynamicForm FormGroup in the app component above.

A nested form group with name and email fields is rendered for each ticket by looping over the tickets form array with the Angular *ngFor directive. Each ticket form group is bound to a containing div element with the directive [formGroup]="ticket".

The form binds the form submit event to the onSubmit() handler in the app component using the Angular event binding (ngSubmit)="onSubmit()". Validation messages are displayed only after the user attempts to submit the form for the first time, this is controlled with the submitted property of the app component.

The reset button click event is bound to the onReset() handler in the app component using the Angular event binding (click)="onReset()", and the clear button is bound to the onClear() handler with (click)="onClear()".

<form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()">
    <div class="card m-3">
        <h5 class="card-header">Angular 10 Dynamic Reactive Forms Example</h5>
        <div class="card-body border-bottom">
            <div class="form-row">
                <div class="form-group">
                    <label>Number of Tickets</label>
                    <select formControlName="numberOfTickets" class="form-control" (change)="onChangeTickets($event)" [ngClass]="{ 'is-invalid': submitted && f.numberOfTickets.errors }">
                        <option value=""></option>
                        <option *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">{{i}}</option>
                    </select>
                    <div *ngIf="submitted && f.numberOfTickets.errors" class="invalid-feedback">
                        <div *ngIf="f.numberOfTickets.errors.required">Number of tickets is required</div>
                    </div>
                </div>
            </div>
        </div>
        <div *ngFor="let ticket of ticketFormGroups; let i = index" class="list-group list-group-flush">
            <div class="list-group-item">
                <h5 class="card-title">Ticket {{i + 1}}</h5>
                <div [formGroup]="ticket" class="form-row">
                    <div class="form-group col-6">
                        <label>Name</label>
                        <input type="text" formControlName="name" class="form-control" [ngClass]="{ 'is-invalid': submitted && ticket.controls.name.errors }" />
                        <div *ngIf="submitted && ticket.controls.name.errors" class="invalid-feedback">
                            <div *ngIf="ticket.controls.name.errors.required">Name is required</div>
                        </div>
                    </div>
                    <div class="form-group col-6">
                        <label>Email</label>
                        <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && ticket.controls.email.errors }" />
                        <div *ngIf="submitted && ticket.controls.email.errors" class="invalid-feedback">
                            <div *ngIf="ticket.controls.email.errors.required">Email is required</div>
                            <div *ngIf="ticket.controls.email.errors.email">Email must be a valid email address</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="card-footer text-center border-top-0">
            <button class="btn btn-primary mr-1">Buy Tickets</button>
            <button class="btn btn-secondary mr-1" type="reset" (click)="onReset()">Reset</button>
            <button class="btn btn-secondary" type="button" (click)="onClear()">Clear</button>
        </div>
    </div>
</form>


Angular 10 App Module

There isn't much going on in the app module other than the standard stuff, the main thing you need to remember for using reactive forms in Angular is to import the ReactiveFormsModule from '@angular/forms' and include it in the imports array of the @NgModule decorator.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

 

Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.

 


Supported by