Angular 14 - Dynamic Reactive Forms Example
Built with Angular 14.2.12
Other versions available:
- Angular: Angular 10, 9, 8
- React: React Hook Form 7, 6, React + Formik
- Vue: Vue + Vuelidate
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 AngularFormControl
that stores the number of tickets selected. It is bound to theselect
input in the app component template with the directiveformControlName="numberOfTickets"
.tickets
is an AngularFormArray
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 thename
and one for theemail
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
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!