You will most likely find that FormGroups
are more than capable of serving your needs for the purpose of combining many FormControl
objects into one container. However, there is one very common pattern that makes its sister type, the FormArray
, extremely useful: variable length cloned inputs.
The code, links, and a live example related to this recipe are available at .
Suppose you had the following skeleton application:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <p>Tags:</p> <ul> <li *ngFor="let t of tagControls; let i = index"> <input [formControl]="t"> </li> </ul> <p><button (click)="addTag()">+</button></p> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { tagControls:Array<FormControl> = []; addTag():void {} saveArticle():void {} }
Your objective is to modify this component so that an arbitrary number of tags can be added and so all the tags can be validated together.
In many ways, a FormArray
behaves more or less identically to a FormGroup
. It is imported in the same way and inherited from AbstractControl
. Also, it is instantiated in a similar way and can add and remove FormControl
instances. First, add the boilerplate to your application; this will allow you to instantiate an instance of a FormArray
and pass it the array of FormControl
objects already inside the component. Since you already have a button that is meant to invoke the addTag
method, you should also configure this method to push a new FormControl
on to tagControl
:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl, FormArray, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <p>Tags:</p> <ul> <li *ngFor="let t of tagControls; let i = index"> <input [formControl]="t"> </li> </ul> <p><button (click)="addTag()">+</button></p> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { tagControls:Array<FormControl> = []; tagFormArray:FormArray = new FormArray(this.tagControls); addTag():void { this.tagFormArray .push(new FormControl(null, Validators.required)); } saveArticle():void {} }
At this point, it's important that you don't confuse yourself with what you are working with. Inside this ArticleEditor
component, you have an array of FormControl
objects (tagControls
) and you also have a single instance of FormArray
(tagFormArray
). The FormArray
instance is initialized by being passed the array of FormControl
objects, which it will then be able to manage.
Now that your FormArray
is managing the tag's FormControl
objects, you can safely use its validator:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl, FormArray, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <p>Tags:</p> <ul> <li *ngFor="let t of tagControls; let i = index"> <input [formControl]="t"> </li> </ul> <p><button (click)="addTag()">+</button></p> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { tagControls:Array<FormControl> = []; tagFormArray:FormArray = new FormArray(this.tagControls); addTag():void { this.tagFormArray .push(new FormControl(null, Validators.required)); } saveArticle():void { if (this.tagFormArray.valid) { alert('Valid!'); } else { alert('Missing field(s)!'); } } }
Because the template is reacting to the click
event, you are able to use Angular data binding to automatically update the template. However, it is extremely important that you note the asymmetry in this example. The template is iterating through the tagControls
array. However, when you want to add a new FormControl
object, you push it to tagFormArray
, which will in turn push it to the tagControls
array. The FormArray
object acts as the manager of the collection of FormControl
objects, and all modifications of this collection should go through the manager, not the collection itself.
The Angular documentation warns you to specifically not modify the underlying FormControl
collection directly. This may lead to undefined data binding behavior, so always be sure to use the FormArray
members push
, insert
, and removeAt
instead of directly manipulating the array of FormControl
objects that you pass upon instantiation.
You can take this example one step further by adding the ability to remove from this list as well. Since you already have the index inside the template repeater and FormArray
offers index-based removal, this is simple to implement:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl, FormArray, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <p>Tags:</p> <ul> <li *ngFor="let t of tagControls; let i = index"> <input [formControl]="t"> <button (click)="removeTag(i)">X</button> </li> </ul> <p><button (click)="addTag()">+</button></p> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { tagControls:Array<FormControl> = []; tagFormArray:FormArray = new FormArray(this.tagControls); addTag():void { this.tagFormArray .push(new FormControl(null, Validators.required)); } removeTag(idx:number):void { this.tagFormArray.removeAt(idx); } saveArticle():void { if (this.tagFormArray.valid) { alert('Valid!'); } else { alert('Missing field(s)!'); } } }
This allows you to cleanly insert and remove FormControl
instances while letting Angular data binding do all of the work for you.