June 25 2019

Angular 8 - Dynamic Reactive Forms Example

Example built with Angular 8.0.2

This is a quick example of how to build a dynamic form with validation in Angular 8 using Reactive Forms. The example is a simple order form for selecting the number of tickets to purchase and entering a name and email address for each ticket. All fields are required and email fields 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 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.

Styling of the example is all done with Bootstrap 4.3 CSS.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-8-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 in the app template below using the [formGroup] directive.

The dynamic form contains two properties:

  • 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 a FormControl for the name and 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 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; }

    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 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 8 Dynamic Reactive Forms Example</h5>
        <div class="card-body">
            <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 t.controls; 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">
            <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>


Dynamic Reactive Forms 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