This article explains step by step how to create a dynamic form in Angular. It covers all essential classes of a reactive form and explains how the FormArray class can be used to create a dynamic form.
Have you ever booked movie tickets online? If yes, then you have used a dynamic form. For example, you select the number of tickets from a drop-down, and then the application asks you to enter information, such as name and age, as many times as the number of tickets selected. Since the number of moviegoers is not fixed and can be changed at the runtime by the user, a dynamic form is needed to gather moviegoer’s information.
In this article, you’ll learn to create a dynamic form in Angular and also hear a high-level explanation of other useful classes of Angular reactive forms. If you are here to learn only about dynamic forms, you may want to jump to the dynamic form section directly.
Angular provides two types of forms:
Reactive forms are more suitable to create a dynamic form. So, let us get started with learning important classes that constitute a reactive form.
To work with reactive forms, you need to add ReactiveFormsModule in the imports array of the AppModule.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
After that, import classes related to the reactive form in the component that is going to contain the form.
import {FormControl,
FormGroup,
FormBuilder,
FormArray,
Validators} from '@angular/forms';
A form contains HTML controls such as input, radio button, drop down, etc. Reactive forms have the FormControl class that represents an individual control. You can use it to create a control as shown in the next code listing:
email: FormControl;
ngOnInit(){
this.email = new FormControl("",Validators.required);
}
In the template, you can use email control as shown below.
<input [formControl]='email' type="text" placeholder="Enter Email" />
{{email.value | json}}
Now, run the application, and you should able to see an input text box that accepts an email value.
The FormArray class is used to create a dynamic form. But before that, let us explore other essential classes that constitute a reactive form. They are:
You have already seen the FormControl class that creates a single control. A FormControl class takes three input parameters:
You can create a FormControl with initial value and required validation as shown in next code listing:
emailControl : FormControl;
defaultLogin = {
email:'debugmode@outlook.com',
password:'282828282'
};
ngOnInit(){
this.emailControl = new FormControl(this.defaultLogin.email,[Validators.required]);
}
In reactive forms, the next important class is FormGroup, which is simply a group of FormControls. You can put many FormControls inside a FormGroup to create a full-fledged form. A FormGroup class corresponds to an HTML form, and FormControl class corresponds to individual control inside the form.
A FormGroup with two controls, email and phone, can be created as shown in the next code listing:
buyTicketForm: FormGroup;
ngOnInit() {
this.buyTicketForm = new FormGroup(
{
emailControl: new FormControl(null, [Validators.required]),
phoneControl: new FormControl(null)
}
)
}
In the component’s template, you can bind it to form and controls as shown below.
<form [formGroup]='buyTicketForm' novalidate class="form" (ngSubmit)='buyTickets()'>
<input formControlName='emailControl' type="text" class="form-control" placeholder="Enter Email" />
<input formControlName='phoneControl' type="text" class="form-control" placeholder="Enter Phone" />
<button class="btn btn-info">Submit</button>
</form>
In the above Form, there is a button to submit the form’s value. When the user clicks on the submit button, buyTickets() function gets executed.
buyTickets() {
if(this.buyTicketForm.status == 'VALID'){
console.log(this.buyTicketForm.value);
}
}
So, you use FormGroup class to encapsulate various FormControl objects, FormArray objects, and also nested FormGroup objects. You can add a nested FormGroup in the buyTicketForm as shown in the next code listing:
buyTicketForm: FormGroup;
ngOnInit() {
this.buyTicketForm = new FormGroup(
{
emailControl: new FormControl(null, [Validators.required]),
phoneControl: new FormControl(null),
address:new FormGroup({
streetControl : new FormControl(),
postalcodeControl: new FormControl()
})
}
)
}
And in the template, you can map the nested FormGroup field address by setting the formGroupName property of the nested form as shown below.
<form [formGroup]='buyTicketForm' novalidate class="form" (ngSubmit)='buyTickets()'>
<input formControlName='emailControl' type="text" class="form-control" placeholder="Enter Email" />
<input formControlName='phoneControl' type="text" class="form-control" placeholder="Enter Phone" />
<form formGroupName='address'>
<input formControlName='streetControl' type="text" class="form-control" placeholder="Enter Street " />
<input formControlName='postalcodeControl' type="number" class="form-control" placeholder="Enter Post Office" />
</form>
<button class="btn btn-info">Submit</button>
</form>
Mainly FormGroup offers API for:
As of now, you have learned about all important classes that constitute a reactive form in Angular.
Creating multiple forms using FormGroup and FormControl can be very lengthy and repetitive. So, to help with it, Angular provides a service called FormBuilder. It provides the syntactic sugar that shortens the syntax to create instances of FormControl, FormGroup and FormArray.
There are three steps to use FormBuilder:
You inject FormBuilder class in the component as shown below:
constructor(private fb: FormBuilder) {
}
After injecting FormBuilder, you can refactor buyTicketForm to use FormBuilder service as shown in the next code listing:
this.buyTicketForm = this.fb.group(
{
emailControl: [null, [Validators.required]],
phoneControl: [null],
address:this.fb.group({
streetControl : [],
postalcodeControl: []
})
}
)
As you’ll notice, that code is now less repetitive. On the other hand, whether you use the FormBuilder class approach or FormGroup class approach, code in the template would be precisely the same. So, to use FormBuilder, you don’t have to make any changes in the template.
Before we go ahead and learn about adding controls dynamically, let us update the form to use bootstrap classes and also add a button to add tickets.
<div class="container">
<br />
<h1 class="text-danger text-center">Buy Tickets</h1>
<div class="row">
<div class="col-md-3">
<button class="btn btn-danger" (click)='addTicket()'>Add Ticket</button>
</div>
</div>
<form [formGroup]='buyTicketForm' novalidate class="text-center border border-light p-5" (ngSubmit)='buyTickets()'>
<input formControlName='emailControl' type="text" class="form-control mb-4" placeholder="Enter Email" />
<input formControlName='phoneControl' type="text" class="form-control mb-4" placeholder="Enter Phone" />
<form formGroupName='address'>
<input formControlName='streetControl' type="text" class="form-control mb-4" placeholder="Enter Street Name" />
<input formControlName='postalcodeControl' type="number" class="form-control mb-4"
placeholder="Enter Postal code " />
</form>
<button class="btn btn-danger">Submit</button>
</form>
</div>
At this point in running the application, you should get a form to buy tickets. Our requirement is each time a user clicks on the Add Ticket button, a new Ticket should get added to the form.
As the user adds tickets at runtime, to handle that, you have to create a dynamic form. A dynamic form may contain either a single control or group of controls. In our example, a ticket contains name and age, so it’s a group of controls. As you have already seen, that group of controls is represented by FormGroup, so let us create a function that returns a FormGroup, that corresponds to a ticket.
createTicket():FormGroup{
return this.fb.group({
name:[null,Validators.required],
age:[null,Validators.required]
})
}
The createTicket function returns a FormGroup that consists of a moviegoer’s name and age. Also, we want to make it so that the user must provide values for name and age fields, so for both the controls have required validation set on it.
The form may contain more than one ticket, so add a new property called tickets
of type FormArray in the buyTicketForm form.
this.buyTicketForm = this.fb.group(
{
emailControl: [null, [Validators.required]],
phoneControl: [null],
address:this.fb.group({
streetControl : [],
postalcodeControl: []
}),
tickets:this.fb.array([this.createTicket()],Validators.required)
}
)
}
In the above form, we are using the FormBuilder array method to create FormArray type control, and its initial value is set by calling the createTicket function. The required validation is also set at the array level so that the user must have to provide values in name and age controls before adding a ticket to the tickets array.
Next, to read of the value of tickets array, add a getter in the component as shown below:
get tickets():FormArray{
return <FormArray> this.buyTicketForm.get('tickets');
}
Also, in the template, there is a button to add a ticket. On click of the button, it pushes a new ticket in the tickets FormArray as shown below.
addTicket() {
this.tickets.push(this.createTicket());
}
So far, we have created a FormArray, put validation on it, created a getter to read its value, and also added a function to push new items in the array.
Tickets are of the type FormArray, and in the template to work with it, you use ngFor structural directive.
<div formArrayName="tickets" *ngFor="let t of tickets.controls; let i = index">
<input formControlName='name' id="{{'name'+i}}" type="text" class="form-control mb-4" placeholder="Enter Name" />
<input formControlName='age' id="{{'age' + i}}" type="number" class="form-control mb-4"
placeholder="Enter Age " />
</div>
A couple of essential points in the above template:
id
must be set dynamically, and interpolation with loop index can be used for that.If the user does not provide a value for name or age control, you can show the validation message as shown below:
<div class="alert alert-danger" *ngIf="tickets.controls[i].get('name').hasError('required') && tickets.controls[i].get('name').touched">
Name is required
</div>
To get a particular control, you use ngFor index value and then name of the control. Putting everything together, the template should look like the below listing:
<div class="container">
<br />
<h1 class="text-danger text-center">Buy Tickets</h1>
<div class="row">
<div class="col-md-3">
<button class="btn btn-danger" (click)='addTicket()'>Add Ticket</button>
</div>
</div>
<form [formGroup]='buyTicketForm' novalidate class="text-center border border-light p-5" (ngSubmit)='buyTickets()'>
<input formControlName='emailControl' type="text" class="form-control mb-4" placeholder="Enter Email" />
<input formControlName='phoneControl' type="text" class="form-control mb-4" placeholder="Enter Phone" />
<form formGroupName='address'>
<input formControlName='streetControl' type="text" class="form-control mb-4" placeholder="Enter Street Name" />
<input formControlName='postalcodeControl' type="number" class="form-control mb-4"
placeholder="Enter Postal code " />
</form>
<div formArrayName="tickets" *ngFor="let t of tickets.controls; let i = index">
<div class="row" [formGroupName]="i">
<div class="col-md-2">
<p class="lead">Ticket {{i+1}}</p>
</div>
<div class="col-md-5">
<input formControlName='name' id="{{'name'+i}}" type="text" class="form-control mb-4"
placeholder="Enter Name" />
</div>
<div class="col-md-5">
<input formControlName='age' id="{{'age' + i}}" type="number" class="form-control mb-4"
placeholder="Enter Age " />
</div>
</div>
<div class="row">
<div class="col-md-2">
</div>
<div class="col-md-5">
<div class="alert alert-danger"
*ngIf="tickets.controls[i].get('name').hasError('required') && tickets.controls[i].get('name').touched">
Name is required
</div>
</div>
<div class="col-md-5">
<div class="alert alert-danger"
*ngIf="tickets.controls[i].get('age').hasError('required') && tickets.controls[i].get('age').touched">
Age is required
</div>
</div>
</div>
</div>
<button class="btn btn-danger" [disabled]='buyTicketForm.invalid'>Submit</button>
</form>
</div>
On running the application, you should now have a fully functional dynamic form. So, in this article, you learned about reactive forms and various classes of them. You also learned about FormArray to create a dynamic form.
I hope you found it useful. Suggestions and comments are welcome.
Dhananjay Kumar is an independent trainer and consultant from India. He is a published author, a well-known speaker, a Google Developer Expert, and a 10-time winner of the Microsoft MVP Award. He is the founder of geek97, which trains developers on various technologies so that they can be job-ready, and organizes India's largest Angular Conference, ng-India. He is the author of the best-selling book on Angular, Angular Essential. Find him on Twitter or GitHub.