Published: December 23 2022

Angular 14 - Dynamic Reactive Forms Example

Built with Angular 14.2.12

Other versions available:

This is a quick example of how to build a dynamic form with validation in Angular 14 using Reactive Forms.

Example Angular 14 App

The example app contains a form to select a number of tickets and enter a name and email for each ticket. When the number of tickets is changed the form dynamically adds/removes fields to match the number selected. Both name and email are required and the email address must be valid.

On submit (buy tickets) if the form is valid the values are simply displayed with a javascript alert. On reset the form is reverted back to its initial state including the resetting the number of tickets to zero. On clear the ticket fields are set to empty the number of tickets is left unchanged.

Validate on Form Submit

I setup the form to display validation messages on form submit rather than as soon as each field is changed (i.e. touched/dirty). This is achieved 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 when the reset or clear button is clicked.

Styled with Bootstrap 5

The example login app is styled with the CSS from Bootstrap 5.2, for more info about Bootstrap see https://getbootstrap.com/docs/5.2/getting-started/introduction/.

Code on GitHub

The project is available on github at https://github.com/cornflourblue/angular-14-dynamic-form-example.

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


Dynamic Reactive Forms 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-root', 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: any) {
        const numberOfTickets = e.target.value || 0;
        if (this.t.length < numberOfTickets) {
            if (this.t.length === 0) {
                // ensure validation messages aren't displaying when the 
                // number of tickets is first selected (changed from zero)
                this.onClear();
            }

            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 Angular Form 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 14 Dynamic Reactive Forms Example</h5>
        <div class="card-body border-bottom">
            <div class="row">
                <div class="col-3 mb-3">
                    <label class="form-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="row">
                    <div class="col-6 mb-3">
                        <label class="form-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="col-6 mb-3">
                        <label class="form-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 me-1">Buy Tickets</button>
            <button class="btn btn-secondary me-1" type="reset" (click)="onReset()">Reset</button>
            <button class="btn btn-secondary" type="button" (click)="onClear()">Clear</button>
        </div>
    </div>
</form>


Angular 14 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 { }

 


Need Some Angular 14 Help?

Search fiverr for freelance Angular 14 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by