Angular Reactive Forms – Dynamic Form Fields using FormArray

In most cases, forms have constant form controls  – remaining static throughout the lifetime of the form. But from time to time, you can find yourself in a situation where you would like to dynamically add or remove form controls. These form controls are added dynamically as result of user interaction. On top of that, you might also want to be able to validate data of the newly created forms. Therefore, having optional fields might not be an option.

In this post, we are going to look at how we can do this in Angular, in Reactive Forms using FormArray. We will be creating a simple angular app, for adding user profile. The user profile form will have the following fields: name, organization (optional) and variable contact information.

The contact information group will have a type of contact (email or phone number), label for the contact and the contact value. We also want to give our users the ability to add more contacts, so that a single profile can have multiple contacts. On top of that, since the type of contact can vary, we also want to change the validator of the contact value based on the selected type.

Getting Started

We will start by creating a new project using Angular CLI.

$ ng new angular-dynamic-form-fields-reactive-forms

Next, we need to install our dependencies. In this case, we will be using bootstrap, the only dependency we need.

$ npm install bootstrap
OR
$ yarn add bootstrap

After installing bootstrap, import it to your angular project inside angular.json, in the style section.

"styles": [
  ...
  "node_modules/bootstrap/scss/bootstrap.scss"
],

And finally, in your app module, import ReactiveFormsModule, which is the only import we need for this post.

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

export class AppModule {}

Now that we have setup our project, let’s build our demo application.

Building a Model for our Reactive Form

We will start by defining a base form group that defines our three form controls – name, organization and contacts. The name and organization form controls won’t change anything dynamically. On the other hand, the contacts form controls will be composed of an array of form groups, with individual form group having 3 form controls.

To achieve this, we will be using FormArray, which accepts an array of Form Groups or Form Controls. We will be grouping the form controls for a single contact, under a single form group. Then this multiple form groups will be added into the  FormArray. Each form group will contain three form controls: Contact Type, Label and Value. The value of the contact can either be a phone number or an email based on the contact type.

First, import the necessary modules we require: FormBuilder, FormArray, FormGroup and Validators.

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

Then, let’s define two properties in our component class: form and contacts properties. The first holds our form model and the second holds our contacts forms array:

public form: FormGroup;
public contactList: FormArray;

Next, let’s define a method to create our contacts form group. The method will return the three form controls, inside a form group.

createContact(): FormGroup {
  return this.fb.group({
    type: ['email', Validators.compose([Validators.required])],
    name: [null, Validators.compose([Validators.required])],
    value: [null, Validators.compose([Validators.required, Validators.email])]
  });
}

And finally, let’s initialize our form, with a single contact as the initial contact. Then users can add more contacts as they wish.

ngOnInit() {
  this.form = this.fb.group({
    name: [null, Validators.compose([Validators.required])],
    organization: [null],
    contacts: this.fb.array([this.createContact()])
  });

  // set contactlist to the form control containing contacts
  this.contactList = this.form.get('contacts') as FormArray;
}

Adding/Removing Form Controls Dynamically using FormArray

Since contact list is an array, it’s a simple matter of pushing new items to the array, just like a normal array. It doesn’t matter whether you are adding a form group, as is our case, or adding a form control, the process is the same.

// add a contact form group
addContact() {
  this.contactList.push(this.createContact());
}

The same goes for removing contacts from the contacts FormArray, it’s a normal array and all you need is the index. We will pass the index of the item we are removing as a parameter for the removeContact() method.

// remove contact from group
removeContact(index) {
  this.contactList.removeAt(index);
}

Changing Form Control Validators Dynamically

If you recall from the beginning, we also wanted to change the validation of the contact in the contacts group based on the type of contact. If it is an email, we validate against email only and if its phone, only against phone numbers. We are trying to avoid a situation where the type of value of the contact and the type of contact are not the same.

But, first we need to create a method to get the form group we want, mainly to make our code more readable. We will call our method getContactsFormGroup(index) method and returns a form group.

getContactsFormGroup(index): FormGroup {
  this.contactList = this.form.get('contacts') as FormArray;
  const formGroup = this.contactList.controls[index] as FormGroup;
  return formGroup;
}

With the above method, instead of writing this in our code to get a contact form group controls value.

const formGroup = this.contactList.controls[index] as FormGroup;
const value = formGroup.controls['value'].value

We only need to write only this:

this.getContactsFormGroup(index).controls['value'].value

This makes our code a lot easier to read and follow, and we are using the same method inside the template for checking validation errors.

Next, we need to add a method to change the validator for the value of the contact, when the type of the contact changes. We will call the method changedContactType(index) and will be triggered by the change event of the form control.

changedContactType(index) {
  let validators = null;

  if (this.getContactsFormGroup(index).controls['type'].value === 'email') {
    validators = Validators.compose([Validators.required, Validators.email]);
  } else {
    validators = Validators.compose([
      Validators.required,
      Validators.pattern(new RegExp('^\\+[0-9]?()[0-9](\\d[0-9]{9})$')) // pattern for validating international phone number
    ]);
  }

  this.getContactsFormGroup(index).controls['value'].setValidators(validators);

  // re-validate the inputs of the form control based on new validation
  this.getContactsFormGroup(index).controls['value'].updateValueAndValidity();
}

Building the Template for Our Form

In most parts, this will be a normal form until you get to rendering the FormArray section. Here, we are going to loop over the contacts FormArray, and render the form groups into a form for user to interact with. We start by defining a FormArray, the same way you define a form group or a form control inside the template. We will be using the formArrayName directive to indicate we are now rendering a FormArray in our template.

<div formArrayName="contacts">
  <!-- Loop Here -->
</div>

Then, let’s go ahead and loop over our contacts FormArray. To do that, we are going to first define a get method, inside our class to fetch the contact FormArray from the form group and typecast it, into a FormArray.

get contactFormGroup() {
  return this.form.get('contacts') as FormArray;
}

Then, we are going to simply loop over the contact FormArray returned by the above get method.

<div class="col-6" *ngFor="let contact of contactFormGroup.controls; let i = index;">
  <div [formGroupName]="i" class="row">
    <!-- Contacts Form controls Here -->
  </div>
</div>

NB: We are using index as the name of the form group. Also, do not use track by method for this loop, as index changes when an item that’s not at the end gets removed. We want to the whole list to be re-rendered when an item is removed or added. This is because, when an item is removed in the middle, the indexes change, and so does our form groups names in the process.

Changing the Type of Contact

In the type of contact field, we are going to have a drop down where the user either selects email or phone. Then, we are going to listen to the changes to this drop down and trigger the method to change form control validation above:

<div class="form-group col-6">
  <label>Type of Contact</label>
  <select (change)="changedContactType(i)" class="form-control" formControlName="type" type="text">
    <option value="email">Email</option>
    <option value="phone">Phone</option>
  </select>
</div>

Then, below our value of contact form control, we are going to check whether the form has all possible errors. Using the hasError() method.

<div class="form-group col-12">
  <label>Email/Phone No.</label>
  <input class="form-control" formControlName="value" type="text">

  <span class="text-danger" *ngIf="getContactsFormGroup(i).controls['value'].touched && getContactsFormGroup(i).controls['value'].hasError('required')">
    Email/Phone no is required!
  </span>

  <span class="text-danger" *ngIf="getContactsFormGroup(i).controls['value'].touched && getContactsFormGroup(i).controls['value'].hasError('email')">
    Email is not valid!
  </span>

  <span class="text-danger" *ngIf="getContactsFormGroup(i).controls['value'].touched && getContactsFormGroup(i).controls['value'].hasError('pattern')">
    Phone no. is not valid!
  </span>
</div>

Source Code and Demo

You can find the code for this project here and the demo here.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.