Thank you for reading my blog posts, I am no longer publishing new content on this platform. You can find my latest content on either mainawycliffe.dev or All Things Typescript Newsletter (✉️)
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.
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
$ 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.
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;
}
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);
}
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();
}
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.
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>
You can find the code for this project here and the demo here.
Angular 7 was officially released this week, with lots of new features. Apart from being officially released, nothing else (feature wise) …
Read MoreIn this post, we are going to look at how we can use APP_INITIALIZER in Angular. So, what exactly does APP_INITIALIZER do? The best way to …
Read MoreAngular provides a default way to configure environment variables, which you can learn more about here. But in a nutshell, you have …
Read MoreAngular apps take time to show meaningful content to the user. This time is mainly after loading the index.html page and bootstrapping the …
Read MoreIn this post, we are going to be creating a simple signup form, with email, password and confirm password controls. We will then validate …
Read MoreIn this post, we are going to look at four important features in angular that can help you during your app development life cycle. These …
Read More