| |

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:

ClassShapeValue
FormControlSingleT
FormGroupObject{ key: T }
FormArrayArrayT[]

What a FormArray tracks:

  • Length — length
  • Elements — at(i), controls
  • Value — value, valueChanges
  • Status — valid, invalid, status
  • State — dirty, touched

What you can do:

MethodPurpose
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. FormArray is a first-class part of the form tree — its validity propagates to the parent, its changes fire valueChanges, 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. FormControl for a simple value; FormGroup for a structured row; FormArray for 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.controls in 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 move exists: Reordering dynamic lists — drag and drop, move up/move down buttons — is common. move handles 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:

BindingUse
[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 addresses
  • formArrayName on 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
  • @for with $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

MethodPurpose
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

PropertyMeaning
lengthNumber of controls
controlsArray of controls
valueArray of values
valueChangesObservable
statusVALID / INVALID / PENDING
valid / invalidBooleans
dirty / touchedState

Creating Arrays

FormMeaning
fb.array([...])From builder
fb.array([])Empty
fb.array([], validators)With validators
new FormArray([...])Manual

Template Directives

DirectiveApplied to
formArrayName="x"Container
[formControlName]="i"Control element
[formGroupName]="i"Group element

Array of What

Element typeBinding
FormControl[formControlName]="i"
FormGroup[formGroupName]="i"
FormArray[formArrayName]="i"

Validators

TypeWhere
Per-controlOn each FormControl
Array-levelOn the FormArray
CustomFunction (control: AbstractControl) => ValidationErrors | null

Common Array Validators

NeedValidator
At least NValidators.minLength(n)
At most NValidators.maxLength(n)
UniqueCustom
All non-emptyPer-control required

Typed Arrays (v14+)

FormType
FormArray<FormControl<string>>Array of string controls
FormArray<FormGroup<{...}>>Array of groups
form.controls.phonesTyped 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

ErrorCause
Cannot find control with pathWrong index binding
formArrayName not foundMissing ReactiveFormsModule
Value is undefinedEmpty array access
Validators not firingWrong level

Dynamic Form Patterns

PatternUse
Add/remove rowspush + removeAt
Reordermove
Replace controlsetControl
Array of groupsfb.group elements
Nested arraysfb.array inside fb.array
Cross-element validationArray-level custom validator

Common Use Cases

CaseArray of
Phone numbersFormControl<string>
TagsFormControl<string>
AddressesFormGroup
Line itemsFormGroup
Question setsFormGroup

Reading Array Values

ExpressionResult
array.valueT[]
array.at(i).valueT
array.controlsAbstractControl[]
array.lengthnumber

Setting Array Values

MethodUse
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

PitfallProblemSolution
Missing formArrayNameElements don’t bindAdd on container
Wrong indexControl not foundUse [formControlName]="i"
Control vs group mixWrong bindingMatch element type
Empty array accessundefinedCheck length
Direct controls pushForm not updatedUse push()
Array-level validator not firingWrong levelAdd to fb.array
No @for trackRe-rendersUse track $index
Missing removal UICan’t remove entriesAdd a button
Forgot validators on new controlsNo validationInclude on push
Duplicate indicesWrong orderUse 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

ConceptMeaning
FormArrayOrdered list of controls
fb.array([...])Create with FormBuilder
push / insertAdd entries
removeAt / clearRemove entries
moveReorder
formArrayNameBind array to template
[formControlName]="i"Bind element by index
[formGroupName]="i"Bind group element
Array-level validatorsOn the array itself
Per-control validatorsOn each element

Key takeaways:

  • FormArray holds an ordered list of controls — value is an array
  • fb.array([...]) creates one with FormBuilder
  • Elements can be FormControl, FormGroup, or nested FormArray
  • Use push, insert, removeAt, clear, move to manage entries
  • Bind with formArrayName on the container and [formControlName]="i" on each input
  • Use [formGroupName]="i" when elements are groups
  • Iterate with @for and $index — or *ngFor with let i = index
  • Per-control validators validate each entry; array-level validators validate the collection
  • Custom validators handle cross-element rules like uniqueness
  • Typed arraysFormArray<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!