Angular 30 🅰️ FormArray and Dynamic Controls
FormGroup handles fixed shapes — the keys are known when you build the form. But real forms often have variable-length lists: multiple phone numbers, a set of tags, a list of line items, a stack of addresses. You don’t know how many at build time; the user adds and removes them. That’s what FormArray is for. A FormArray is an ordered list of controls — each element can be a FormControl, a FormGroup, or another FormArray. Adding, removing, and reordering entries is a first-class operation. This chapter covers building dynamic forms with FormArray, the template patterns that render and control them, and the validation that makes them safe.
Key point: A FormArray is like a FormGroup whose keys are indices (0, 1, 2, …) instead of names. Its value is an array, not an object. Elements can be any kind of control. The template renders the array with formArrayName on a container and [formControlName]="i" on each element (or formGroupName="i" for nested groups). Add with push(), remove with removeAt(), insert with insert(), move with move(). The array is dynamic; the form stays in sync.
What a FormArray is
A FormArray is an ordered list of controls.
import { FormArray, FormControl } from '@angular/forms';
const phones = new FormArray([
new FormControl('555-0100'),
new FormControl('555-0101')
]);
phones.value; // ['555-0100', '555-0101']
phones.length; // 2
phones.at(0); // FormControl
Every element is a control. The array’s value is an array.
Comparing the three:
| Class | Shape | Value |
|---|---|---|
FormControl | Single | T |
FormGroup | Object | { key: T } |
FormArray | Array | T[] |
What a FormArray tracks:
- Length —
length - Elements —
at(i),controls - Value —
value,valueChanges - Status —
valid,invalid,status - State —
dirty,touched
What you can do:
| Method | Purpose |
|---|---|
push(control) | Append |
insert(i, control) | Insert at index |
removeAt(i) | Remove at index |
clear() | Remove all |
setControl(i, control) | Replace at index |
at(i) | Access |
move(from, to) | Reorder |
Why FormArray matters: Dynamic lists require a dynamic container. FormArray is that container — order matters, length changes, and the form tracks everything. Without it, you’d hand-roll arrays of controls and manage indices yourself.
Why “FormArray”: It’s an array of form controls. The name describes both — array of forms.
Why not a plain array of FormControls: A plain array wouldn’t participate in the form’s validation, status, or change tracking.
FormArrayis a first-class part of the form tree — its validity propagates to the parent, its changes firevalueChanges, and its state is tracked just like any other control.
Creating a FormArray
Create with FormBuilder.array or new FormArray.
With FormBuilder:
import { FormBuilder, FormControl } from '@angular/forms';
const fb = new FormBuilder();
const form = fb.group({
name: [''],
phones: fb.array([
fb.control('555-0100')
])
});
Each element in fb.array is a control — typically from fb.control, fb.group, or another fb.array.
With new FormArray:
import { FormArray, FormControl, FormGroup } from '@angular/forms';
const phones = new FormArray<FormControl<string>>([
new FormControl('555-0100', { nonNullable: true })
]);
The generic type parameter describes the element type.
Empty array:
const phones = fb.array([]);
Starts empty; the user adds entries.
Array of groups:
const addresses = fb.array([
fb.group({
street: [''],
city: ['']
})
]);
Each element is a FormGroup. The value is an array of objects.
Typed arrays (Angular 14+):
const phones = new FormArray<FormControl<string>>([]);
const addresses = new FormArray<FormGroup<{
street: FormControl<string>;
city: FormControl<string>;
}>>([]);
Types flow through.
Why FormBuilder: The array syntax is compact and consistent with the rest of the form. fb.array([...]) reads cleanly.
Why type parameters: Typed arrays give you typed .value, typed .at(i), and compile-time checks when reading values.
Why arrays can hold anything: Each element is a control — any control.
FormControlfor a simple value;FormGroupfor a structured row;FormArrayfor nested lists. The array’s type parameter reflects what it holds.
Accessing a FormArray
Get the array from the parent form.
const form = fb.group({
phones: fb.array([fb.control('')])
});
const phones = form.get('phones') as FormArray;
With typed forms:
form = this.fb.group({
phones: this.fb.array<FormControl<string>>([
this.fb.control('', { nonNullable: true })
])
});
get phones(): FormArray<FormControl<string>> {
return this.form.controls.phones;
}
The getter returns the typed array.
Accessing elements:
this.phones.at(0); // FormControl
this.phones.controls[0]; // same
this.phones.length; // number
Accessing values:
this.phones.value; // string[]
Why a getter: this.form.controls.phones is verbose. A getter get phones() shortens the template.
Why cast: Without typed forms, form.get('phones') returns AbstractControl | null. Cast to FormArray — or use typed forms so the getter knows.
Why typed getters: Typed forms infer the array’s element type. get phones(): FormArray<FormControl<string>> gives full type safety.
Why not iterate
form.controls.phones.controlsin the template: You can, but a getter is cleaner. And the array’s typed shape helps the template type-check each element.
Adding and removing entries
Dynamic lists change length. The methods are push, insert, removeAt, clear.
Push — append:
addPhone(): void {
this.phones.push(this.fb.control('', { nonNullable: true }));
}
Adds a new empty control at the end.
Insert — at index:
addPhoneAt(index: number): void {
this.phones.insert(index, this.fb.control('', { nonNullable: true }));
}
Inserts before the given index.
Remove — by index:
removePhone(index: number): void {
this.phones.removeAt(index);
}
Removes the control and its DOM node.
Clear — remove all:
this.phones.clear();
Replace — at index:
this.phones.setControl(0, this.fb.control('new'));
Replaces the control at index 0.
Move — reorder:
this.phones.move(0, 2);
Moves the control from index 0 to index 2.
Why index-based: Arrays are ordered. The index is the position. Removing at 1 shifts everything after it.
Why removeAt and not remove(control): Angular 15+ added remove(control) for removing by identity. removeAt(i) is the classic API. Both work.
Why the array updates the DOM: The template iterates controls. When the array changes, the iteration re-renders. Angular tracks by position.
Why
moveexists: Reordering dynamic lists — drag and drop, move up/move down buttons — is common.movehandles the array reorder and the template updates.
Rendering a FormArray
The template renders the array with formArrayName and per-element bindings.
Array of controls:
<div formArrayName="phones">
<div *ngFor="let phone of phones.controls; let i = index">
<input [formControlName]="i">
<button type="button" (click)="removePhone(i)">Remove</button>
</div>
</div>
Each input binds to the control at index i.
The directive: formArrayName="phones" provides the array to the children. [formControlName]="i" binds an individual input to the array’s element at index i.
Array of groups:
<div formArrayName="addresses">
<div *ngFor="let addr of addresses.controls; let i = index" [formGroupName]="i">
<input formControlName="street">
<input formControlName="city">
<button type="button" (click)="removeAddress(i)">Remove</button>
</div>
</div>
Each row binds to a FormGroup at index i with [formGroupName]="i".
The distinction:
| Binding | Use |
|---|---|
[formControlName]="i" | Element is a FormControl |
[formGroupName]="i" | Element is a FormGroup |
With @for:
<div formArrayName="phones">
@for (phone of phones.controls; track $index; let i = $index) {
<input [formControlName]="i">
}
</div>
The modern control flow syntax. $index becomes the binding.
Why formArrayName on the container: It marks the region as belonging to the array. Children inside can bind to elements by index.
Why controls in the loop: phones.controls is the array of controls. The template iterates them. Each iteration gets one control by index.
Why i (not the control itself): formControlName and formGroupName take a name or index. [formControlName]="i" uses the index within the array.
Why the template needs the index: FormArray elements have no names — only positions. The index is the “name” for binding. That’s the crux of rendering a FormArray.
Validating a FormArray
Validators attach to individual controls, the array itself, or both.
Per-control validators:
addPhone(): void {
this.phones.push(
this.fb.control('', [Validators.required, Validators.pattern(/^[\d-]+$/)])
);
}
Each new control has its own validators.
Array-level validators:
const phones = this.fb.array([], [Validators.minLength(1)]);
The array itself must have at least one control.
Combined:
form = this.fb.group({
phones: this.fb.array(
[this.fb.control('', Validators.required)],
[Validators.minLength(1), Validators.maxLength(5)]
)
});
Each control is required; the array must have between 1 and 5 entries.
Custom array validators:
function uniqueValues(control: AbstractControl): ValidationErrors | null {
const array = control as FormArray;
const values = array.controls.map(c => c.value);
const hasDuplicates = new Set(values).size !== values.length;
return hasDuplicates ? { uniqueValues: true } : null;
}
const phones = this.fb.array([], uniqueValues);
The validator runs when the array changes.
The form’s status: A FormArray is valid if all its controls are valid and its own validators pass.
this.phones.valid; // true if all controls and array validators pass
this.phones.invalid; // true if any fail
Why validators on controls: Each element is a separate control. Its validators fire independently.
Why validators on the array: The array’s own rules — minimum length, maximum length, uniqueness — are properties of the collection, not any single control.
Why a custom validator: Standard validators don’t know about cross-element rules. A custom validator has the array in hand and can check anything.
Why both are needed: A required validator on each control ensures no entry is empty. A minLength on the array ensures at least one entry exists. Together they express “at least one, and every entry is valid.”
A full example
A dynamic contact form with multiple phone numbers and addresses.
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators, FormArray, FormControl, FormGroup, AbstractControl, ValidationErrors } from '@angular/forms';
import { CommonModule } from '@angular/common';
function uniquePhones(control: AbstractControl): ValidationErrors | null {
const array = control as FormArray<FormControl<string>>;
const values = array.controls
.map(c => c.value)
.filter(v => v.length > 0);
const hasDuplicates = new Set(values).size !== values.length;
return hasDuplicates ? { uniquePhones: true } : null;
}
@Component({
selector: 'app-contact',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label>
Name
<input formControlName="name">
</label>
<fieldset formArrayName="phones">
<legend>Phones</legend>
@for (phone of phones.controls; track $index; let i = $index) {
<div class="row">
<input [formControlName]="i" placeholder="Phone number">
<button type="button" (click)="removePhone(i)">×</button>
@if (phone.invalid && phone.touched) {
<span class="error">Invalid phone</span>
}
</div>
}
@if (phones.errors?.['uniquePhones'] && phones.touched) {
<span class="error">Phone numbers must be unique</span>
}
<button type="button" (click)="addPhone()">Add phone</button>
</fieldset>
<fieldset formArrayName="addresses">
<legend>Addresses</legend>
@for (addr of addresses.controls; track $index; let i = $index) {
<div class="row" [formGroupName]="i">
<input formControlName="street" placeholder="Street">
<input formControlName="city" placeholder="City">
<button type="button" (click)="removeAddress(i)">×</button>
</div>
}
<button type="button" (click)="addAddress()">Add address</button>
</fieldset>
<button type="submit" [disabled]="form.invalid">Submit</button>
<p>Status: {{ form.status }}</p>
</form>
`
})
export class ContactComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', Validators.required],
phones: this.fb.array(
[this.fb.control('', { nonNullable: true })],
uniquePhones
),
addresses: this.fb.array([
this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required]
})
])
});
get phones(): FormArray {
return this.form.controls.phones;
}
get addresses(): FormArray {
return this.form.controls.addresses;
}
addPhone(): void {
this.phones.push(
this.fb.control('', [Validators.required, Validators.pattern(/^[\d-]+$/)])
);
}
removePhone(index: number): void {
this.phones.removeAt(index);
}
addAddress(): void {
this.addresses.push(
this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required]
})
);
}
removeAddress(index: number): void {
this.addresses.removeAt(index);
}
onSubmit(): void {
if (this.form.invalid) return;
console.log(this.form.value);
}
}
What this shows:
fb.array([...])for both phones and addressesformArrayNameon the container[formControlName]="i"for phone inputs (control elements)[formGroupName]="i"for address rows (group elements)uniquePhones— an array-level custom validator- Add/remove methods managing the arrays
@forwith$index— the modern iteration
Two different element types — a FormControl and a FormGroup — in the same form.
Why this shape: It’s a realistic dynamic form. Phones are simple strings; addresses are structured objects. The array pattern handles both. Adding and removing entries updates the form and the template.
Complete Example Session
# ============================================
# PART 1: BASIC FORM ARRAY
# ============================================
cat > basic-array.ts << 'EOF'
import { FormArray, FormControl } from '@angular/forms';
const phones = new FormArray([
new FormControl('555-0100'),
new FormControl('555-0101')
]);
console.log(phones.value);
console.log(phones.length);
console.log(phones.at(0)?.value);
EOF
npx tsc --noEmit basic-array.ts
# (no errors)
# ============================================
# PART 2: FORMBUILDER ARRAY
# ============================================
cat > builder-array.ts << 'EOF'
import { FormBuilder } from '@angular/forms';
const fb = new FormBuilder();
const form = fb.group({
name: [''],
phones: fb.array([
fb.control('555-0100')
])
});
console.log(form.value);
EOF
npx tsc --noEmit builder-array.ts
# (no errors)
# ============================================
# PART 3: ADD AND REMOVE
# ============================================
cat > add-remove.ts << 'EOF'
import { FormBuilder } from '@angular/forms';
const fb = new FormBuilder();
const form = fb.group({
phones: fb.array<string>([])
});
const phones = form.controls.phones;
phones.push(fb.control('111', { nonNullable: true }));
phones.push(fb.control('222', { nonNullable: true }));
console.log(phones.value); // ['111', '222']
phones.removeAt(0);
console.log(phones.value); // ['222']
phones.insert(0, fb.control('000', { nonNullable: true }));
console.log(phones.value); // ['000', '222']
phones.move(0, 1);
console.log(phones.value); // ['222', '000']
EOF
npx tsc --noEmit add-remove.ts
# (no errors)
# ============================================
# PART 4: ARRAY OF GROUPS
# ============================================
cat > groups.ts << 'EOF'
import { FormBuilder } from '@angular/forms';
const fb = new FormBuilder();
const form = fb.group({
addresses: fb.array([
fb.group({
street: [''],
city: ['']
})
])
});
const addresses = form.controls.addresses;
addresses.push(fb.group({
street: [''],
city: ['']
}));
console.log(form.value);
EOF
npx tsc --noEmit groups.ts
# (no errors)
# ============================================
# PART 5: VALIDATORS ON ARRAY
# ============================================
cat > validators.ts << 'EOF'
import { FormBuilder, Validators, AbstractControl, ValidationErrors, FormArray } from '@angular/forms';
function atLeastOne(control: AbstractControl): ValidationErrors | null {
const array = control as FormArray;
return array.length >= 1 ? null : { atLeastOne: true };
}
const fb = new FormBuilder();
const form = fb.group({
phones: fb.array<string>([], { validators: [atLeastOne] })
});
const phones = form.controls.phones;
console.log(phones.valid); // false
console.log(phones.errors); // { atLeastOne: true }
phones.push(fb.control('555', { nonNullable: true }));
console.log(phones.valid); // true
EOF
npx tsc --noEmit validators.ts
# (no errors)
# ============================================
# PART 6: UNIQUE VALUES VALIDATOR
# ============================================
cat > unique.ts << 'EOF'
import { FormBuilder, AbstractControl, ValidationErrors, FormArray, FormControl } from '@angular/forms';
function uniqueValues(control: AbstractControl): ValidationErrors | null {
const array = control as FormArray<FormControl<string>>;
const values = array.controls.map(c => c.value).filter(v => v.length > 0);
return new Set(values).size === values.length ? null : { unique: true };
}
const fb = new FormBuilder();
const form = fb.group({
tags: fb.array<FormControl<string>>([], { validators: [uniqueValues] })
});
const tags = form.controls.tags;
tags.push(fb.control('a', { nonNullable: true }));
tags.push(fb.control('b', { nonNullable: true }));
console.log(tags.valid); // true
tags.push(fb.control('a', { nonNullable: true }));
console.log(tags.valid); // false
console.log(tags.errors); // { unique: true }
EOF
npx tsc --noEmit unique.ts
# (no errors)
# ============================================
# PART 7: TYPED ARRAY
# ============================================
cat > typed.ts << 'EOF'
import { FormArray, FormControl } from '@angular/forms';
const phones = new FormArray<FormControl<string>>([]);
phones.push(new FormControl('111', { nonNullable: true }));
phones.push(new FormControl('222', { nonNullable: true }));
const values: string[] = phones.value;
console.log(values);
const first: string = phones.at(0).value;
console.log(first);
EOF
npx tsc --noEmit typed.ts
# (no errors)
# ============================================
# PART 8: RUN AND OBSERVE
# ============================================
npx tsc basic-array.ts builder-array.ts add-remove.ts groups.ts validators.ts unique.ts typed.ts
node basic-array.js
# [ [ '555-0100', '555-0101' ] ]
# [ 2 ]
# [ 555-0100 ]
node builder-array.js
# [ { name: '', phones: [ '555-0100' ] } ]
node add-remove.js
# [ [ '111', '222' ] ]
# [ [ '222' ] ]
# [ [ '000', '222' ] ]
# [ [ '222', '000' ] ]
node groups.js
# [ { addresses: [ { street: '', city: '' }, { street: '', city: '' } ] } ]
node validators.js
# [ false ]
# [ { atLeastOne: true } ]
# [ true ]
node unique.js
# [ true ]
# [ false ]
# [ { unique: true } ]
node typed.js
# [ [ '111', '222' ] ]
# [ 111 ]
Quick Reference
FormArray Methods
| Method | Purpose |
|---|---|
push(control) | Append |
insert(i, control) | Insert at index |
removeAt(i) | Remove at index |
remove(control) | Remove by identity (v15+) |
clear() | Remove all |
setControl(i, control) | Replace |
at(i) | Access |
move(from, to) | Reorder |
FormArray Properties
| Property | Meaning |
|---|---|
length | Number of controls |
controls | Array of controls |
value | Array of values |
valueChanges | Observable |
status | VALID / INVALID / PENDING |
valid / invalid | Booleans |
dirty / touched | State |
Creating Arrays
| Form | Meaning |
|---|---|
fb.array([...]) | From builder |
fb.array([]) | Empty |
fb.array([], validators) | With validators |
new FormArray([...]) | Manual |
Template Directives
| Directive | Applied to |
|---|---|
formArrayName="x" | Container |
[formControlName]="i" | Control element |
[formGroupName]="i" | Group element |
Array of What
| Element type | Binding |
|---|---|
FormControl | [formControlName]="i" |
FormGroup | [formGroupName]="i" |
FormArray | [formArrayName]="i" |
Validators
| Type | Where |
|---|---|
| Per-control | On each FormControl |
| Array-level | On the FormArray |
| Custom | Function (control: AbstractControl) => ValidationErrors | null |
Common Array Validators
| Need | Validator |
|---|---|
| At least N | Validators.minLength(n) |
| At most N | Validators.maxLength(n) |
| Unique | Custom |
| All non-empty | Per-control required |
Typed Arrays (v14+)
| Form | Type |
|---|---|
FormArray<FormControl<string>> | Array of string controls |
FormArray<FormGroup<{...}>> | Array of groups |
form.controls.phones | Typed getter |
@for Iteration
<div formArrayName="phones">
@for (phone of phones.controls; track $index; let i = $index) {
<input [formControlName]="i">
}
</div>
Getter Pattern
get phones(): FormArray {
return this.form.controls.phones;
}
Or typed:
get phones(): FormArray<FormControl<string>> {
return this.form.controls.phones;
}
Common Errors
| Error | Cause |
|---|---|
Cannot find control with path | Wrong index binding |
formArrayName not found | Missing ReactiveFormsModule |
Value is undefined | Empty array access |
| Validators not firing | Wrong level |
Dynamic Form Patterns
| Pattern | Use |
|---|---|
| Add/remove rows | push + removeAt |
| Reorder | move |
| Replace control | setControl |
| Array of groups | fb.group elements |
| Nested arrays | fb.array inside fb.array |
| Cross-element validation | Array-level custom validator |
Common Use Cases
| Case | Array of |
|---|---|
| Phone numbers | FormControl<string> |
| Tags | FormControl<string> |
| Addresses | FormGroup |
| Line items | FormGroup |
| Question sets | FormGroup |
Reading Array Values
| Expression | Result |
|---|---|
array.value | T[] |
array.at(i).value | T |
array.controls | AbstractControl[] |
array.length | number |
Setting Array Values
| Method | Use |
|---|---|
setValue(array) | Replace all |
patchValue(array) | Update existing |
reset() | Back to initial |
Testing Arrays
const form = fb.group({
phones: fb.array([fb.control('')])
});
form.controls.phones.push(fb.control('555'));
expect(form.controls.phones.length).toBe(2);
No DOM needed.
Best Practices
✅ Do This:
// Use FormBuilder.array
phones: this.fb.array([]) // ✅
// Use a getter for the template
get phones(): FormArray { return this.form.controls.phones; } // ✅
// Use typed arrays
new FormArray<FormControl<string>>([]) // ✅
// Use `[formControlName]="i"` for control elements
<input [formControlName]="i"> // ✅
// Use `[formGroupName]="i"` for group elements
<div [formGroupName]="i"> // ✅
// Track by $index in @for
@for (phone of phones.controls; track $index; let i = $index) { }// ✅
// Validate each control
this.fb.control('', Validators.required) // ✅
// Add array-level validators
this.fb.array([], atLeastOne) // ✅
// Use removeAt for removal
this.phones.removeAt(i); // ✅
// Test arrays directly
expect(phones.length).toBe(2); // ✅
❌ Don’t Do This:
// Don't render the array without formArrayName
<div> // ⚠️ no formArrayName // ⚠️
<input formControlName="0"> // ⚠️ wrong syntax
</div>
// Don't use formControlName with the control object
<input [formControlName]="phone"> // ⚠️ use [formControl] // ⚠️
// Don't forget the index
<input formControlName="phone"> // ⚠️ no index // ⚠️
// Don't mutate the controls array directly
this.phones.controls.push(control); // ⚠️ use push() // ⚠️
// Don't set values without setValue
this.phones.value = [...]; // ⚠️ use setValue or patchValue // ⚠️
// Don't skip validators on dynamic controls
this.fb.control(''); // ⚠️ no validation // ⚠️
// Don't render huge arrays without tracking
@for (x of arr; track $index) { } // OK for indices // ✅
// Don't ignore empty state
// Show an "add first item" prompt // ⚠️
// Don't use `FormArray<any>`
new FormArray<any>([]) // ⚠️ untyped // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing formArrayName | Elements don’t bind | Add on container |
| Wrong index | Control not found | Use [formControlName]="i" |
| Control vs group mix | Wrong binding | Match element type |
| Empty array access | undefined | Check length |
| Direct controls push | Form not updated | Use push() |
| Array-level validator not firing | Wrong level | Add to fb.array |
No @for track | Re-renders | Use track $index |
| Missing removal UI | Can’t remove entries | Add a button |
| Forgot validators on new controls | No validation | Include on push |
| Duplicate indices | Wrong order | Use removeAt or move |
Real-World Examples
1. Basic array
const phones = fb.array([fb.control('')]);
2. Empty array
const tags = fb.array<string>([]);
3. Array of groups
const addresses = fb.array([
fb.group({ street: [''], city: [''] })
]);
4. Add control
phones.push(fb.control('', Validators.required));
5. Remove control
phones.removeAt(i);
6. Insert at index
phones.insert(0, fb.control(''));
7. Move
phones.move(0, 2);
8. Clear
phones.clear();
9. Getter
get phones(): FormArray { return this.form.controls.phones; }
10. Template
<div formArrayName="phones">
@for (p of phones.controls; track $index; let i = $index) {
<input [formControlName]="i">
}
</div>
11. Remove button
<button (click)="removePhone(i)">×</button>
12. Add button
<button type="button" (click)="addPhone()">Add</button>
13. Validators on each
fb.control('', [Validators.required, Validators.email])
14. Array-level validator
fb.array([], [Validators.minLength(1)])
15. Custom validator
function uniqueValues(control: AbstractControl): ValidationErrors | null {
const array = control as FormArray;
const values = array.controls.map(c => c.value);
return new Set(values).size === values.length ? null : { unique: true };
}
16. Array of address groups
addresses: this.fb.array([
this.fb.group({
street: [''],
city: ['']
})
])
17. Render group elements
<div [formGroupName]="i">
<input formControlName="street">
<input formControlName="city">
</div>
18. Empty state
@if (phones.length === 0) {
<p>No phone numbers yet.</p>
}
19. Track by index
@for (p of phones.controls; track $index; let i = $index) { }
20. Typed getter
get phones(): FormArray<FormControl<string>> {
return this.form.controls.phones;
}
Visual: FormArray Structure
┌──────────────────────────────────────────────┐
│ FormGroup: user │
│ ┌────────────────────────────────────────┐ │
│ │ FormControl: name │ │
│ │ FormControl: email │ │
│ │ FormArray: phones │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ FormControl: '555-0100' │ │ │
│ │ │ FormControl: '555-0101' │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ FormArray: addresses │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ FormGroup: { street, city } │ │ │
│ │ │ FormGroup: { street, city } │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
Value:
{
name: '',
email: '',
phones: ['555-0100', '555-0101'],
addresses: [
{ street: '', city: '' },
{ street: '', city: '' }
]
}
Visual: FormGroup vs FormArray
┌──────────────────────────────────────────────┐
│ FormGroup │
│ │
│ Keys: names ('name', 'email') │
│ Value: object { name: ..., email: ... } │
│ Shape: fixed │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ FormArray │
│ │
│ Keys: indices (0, 1, 2, ...) │
│ Value: array [v0, v1, v2] │
│ Shape: dynamic │
│ │
└──────────────────────────────────────────────┘
Visual: Template Rendering
┌──────────────────────────────────────────────┐
│ <div formArrayName="phones"> │
│ │
│ @for (p of phones.controls; │
│ track $index; let i = $index) { │
│ │
│ <input [formControlName]="i"> │
│ │
│ } │
│ │
│ </div> │
│ │
│ The formArrayName provides the array │
│ [formControlName]="i" binds each input │
│ │
└──────────────────────────────────────────────┘
Visual: Control vs Group Elements
┌──────────────────────────────────────────────┐
│ FormArray<FormControl<string>> │
│ │
│ phones.controls = [ │
│ FormControl, │
│ FormControl, │
│ FormControl │
│ ] │
│ │
│ Template: │
│ <input [formControlName]="i"> │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ FormArray<FormGroup> │
│ │
│ addresses.controls = [ │
│ FormGroup, │
│ FormGroup │
│ ] │
│ │
│ Template: │
│ <div [formGroupName]="i"> │
│ <input formControlName="street"> │
│ </div> │
│ │
└──────────────────────────────────────────────┘
Visual: Add and Remove Flow
┌──────────────────────────────────────────────┐
│ Initial: phones.length = 1 │
│ │
│ [FormControl] │
│ │
└──────────────────────────────────────────────┘
│
│ addPhone()
▼
┌──────────────────────────────────────────────┐
│ After: phones.length = 2 │
│ │
│ [FormControl, FormControl] │
│ │
└──────────────────────────────────────────────┘
│
│ removeAt(0)
▼
┌──────────────────────────────────────────────┐
│ After: phones.length = 1 │
│ │
│ [FormControl] │
│ │
│ The remaining control is now at index 0 │
│ │
└──────────────────────────────────────────────┘
Visual: Array Validators
┌──────────────────────────────────────────────┐
│ Array-level validators run on the array │
│ │
│ fb.array([...], [Validators.minLength(1)]) │
│ │
│ → at least 1 element │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Per-control validators run on each element │
│ │
│ fb.control('', Validators.required) │
│ │
│ → each element must be non-empty │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Combined: │
│ Array has ≥1 element AND each is valid │
│ │
└──────────────────────────────────────────────┘
Visual: Custom Unique Validator
┌──────────────────────────────────────────────┐
│ Array value: ['a', 'b', 'c'] │
│ │
│ uniqueValues → null (no duplicates) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Array value: ['a', 'b', 'a'] │
│ │
│ uniqueValues → { unique: true } │
│ │
│ Form invalid, error surfaced │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Fixed keys? │
│ └── FormGroup │
│ │
│ Ordered, variable-length? │
│ └── FormArray │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ What does the array hold? │
│ │ │
│ ├── Simple values ──► FormControl │
│ │ │
│ └── Structured rows ──► FormGroup │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need validation? │
│ │ │
│ ├── Per element ──► on control │
│ │ │
│ ├── Array length ──► minLength/maxLength│
│ │ │
│ └── Cross-element ──► custom validator │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Rendering the array? │
│ │ │
│ ├── Control elements ──► [formControlName]="i"│
│ │ │
│ └── Group elements ──► [formGroupName]="i"│
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
FormArray | Ordered list of controls |
fb.array([...]) | Create with FormBuilder |
push / insert | Add entries |
removeAt / clear | Remove entries |
move | Reorder |
formArrayName | Bind array to template |
[formControlName]="i" | Bind element by index |
[formGroupName]="i" | Bind group element |
| Array-level validators | On the array itself |
| Per-control validators | On each element |
Key takeaways:
FormArrayholds an ordered list of controls — value is an arrayfb.array([...])creates one withFormBuilder- Elements can be
FormControl,FormGroup, or nestedFormArray - Use
push,insert,removeAt,clear,moveto manage entries - Bind with
formArrayNameon the container and[formControlName]="i"on each input - Use
[formGroupName]="i"when elements are groups - Iterate with
@forand$index— or*ngForwithlet i = index - Per-control validators validate each entry; array-level validators validate the collection
- Custom validators handle cross-element rules like uniqueness
- Typed arrays —
FormArray<FormControl<string>>— give type safety - Add/remove buttons drive the array from the template
- Test arrays directly — no TestBed needed for the logic
Remember: FormArray is the container for dynamic lists. Its elements are controls; its value is an array; its indices are the keys. push and removeAt change the length; formArrayName and [formControlName]="i" render each entry. Add array-level validators for collection rules and per-control validators for individual entries. Combined with FormGroup and FormControl, FormArray lets you model any shape — fixed or dynamic — with the same reactive forms API.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!