Skip to content
intermediate Phase 7 · Reactive Forms and Validation

Dynamic Fields with FormArray

Add and remove repeating rows of inputs at runtime.

45m
0 problems
Topic Progress 0%

Creating and Using FormArray

FormArray holds a dynamic list of form controls. It is ideal for repeating rows like cart items, phone numbers, or addresses.

Creating a FormArray

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

@Component({
  selector: 'app-order-form',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="orderForm" (ngSubmit)="onSubmit()">
      <div formArrayName="items">
        @for (item of items.controls; track item; let i = $index) {
          <div [formGroupName]="i">
            <input formControlName="product" placeholder="Product">
            <input formControlName="quantity" type="number" placeholder="Qty">
            <button type="button" (click)="removeItem(i)">Remove</button>
          </div>
        }
      </div>

      <button type="button" (click)="addItem()">Add Item</button>
      <button type="submit">Submit Order</button>
    </form>
  `,
})
export class OrderFormComponent {
  private fb = inject(FormBuilder);

  orderForm = this.fb.group({
    items: this.fb.array([this.createItem()]),
  });

  get items(): FormArray {
    return this.orderForm.get('items') as FormArray;
  }

  createItem(): FormGroup {
    return this.fb.group({
      product: ['', Validators.required],
      quantity: [1, [Validators.required, Validators.min(1)]],
    });
  }

  addItem(): void {
    this.items.push(this.createItem());
  }

  removeItem(index: number): void {
    this.items.removeAt(index);
  }

  onSubmit(): void {
    if (this.orderForm.valid) {
      console.log('Order:', this.orderForm.value);
    }
  }
}

FormArray Methods

Method Purpose
push(control) Add a control to the end
removeAt(index) Remove control at index
insert(index, control) Insert control at index
at(index) Get control at index
controls Get all controls array
length Get number of controls
reset() Reset all controls
clear() Remove all controls

Validating FormArray Contents

FormArray supports both per-item validation and array-level validation.

Per-Item Validation

createItem(): FormGroup {
  return this.fb.group({
    product: ['', [Validators.required]],
    quantity: [1, [Validators.required, Validators.min(1), Validators.max(99)]],
  });
}

Array-Level Validation

export function minItemsValidator(min: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const array = control as FormArray;
    return array.length >= min ? null : { minItems: { required: min, actual: array.length } };
  };
}

this.orderForm = this.fb.group({
  items: this.fb.array([this.createItem()], minItemsValidator(1)),
});

Displaying Validation Errors for Array Items

<div formArrayName="items">
  @for (item of items.controls; track item; let i = $index) {
    <div [formGroupName]="i">
      <input formControlName="product" placeholder="Product">

      @if (item.get('product')?.invalid && item.get('product')?.touched) {
        <p class="error">Product is required</p>
      }

      <input formControlName="quantity" type="number">

      @if (item.get('quantity')?.invalid && item.get('quantity')?.touched) {
        <p class="error">Quantity must be between 1 and 99</p>
      }
    </div>
  }
</div>

Complete Example: Dynamic Phone Numbers

@Component({
  selector: 'app-phone-form',
  template: `
    <form [formGroup]="form">
      <div formArrayName="phones">
        @for (phone of phones.controls; track phone; let i = $index) {
          <div [formGroupName]="i">
            <input formControlName="number" placeholder="Phone number">
            <select formControlName="type">
              <option value="mobile">Mobile</option>
              <option value="home">Home</option>
              <option value="work">Work</option>
            </select>
            <button type="button" (click)="removePhone(i)">Remove</button>
          </div>
        }
      </div>
      <button type="button" (click)="addPhone()">Add Phone</button>
    </form>
  `,
})
export class PhoneFormComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    phones: this.fb.array([this.createPhone()]),
  });

  get phones(): FormArray {
    return this.form.get('phones') as FormArray;
  }

  createPhone(): FormGroup {
    return this.fb.group({
      number: ['', [Validators.required, Validators.pattern(/^\d{10}$/)]],
      type: ['mobile'],
    });
  }

  addPhone(): void {
    this.phones.push(this.createPhone());
  }

  removePhone(index: number): void {
    this.phones.removeAt(index);
  }
}

Quiz

1. What is FormArray used for?

Question 1 options

2. How do you add a new control to a FormArray?

Question 2 options

3. How do you render FormArray controls in a template?

Question 3 options

Flashcards

Question

What is FormArray for?

Answer

Dynamic lists of form controls. Add with push(), remove with removeAt().

Question

How do you render FormArray in a template?

Answer

formArrayName on container, [formGroupName]="i" on each item.

Question

How do you validate FormArray contents?

Answer

Per-item validators on each FormGroup, or array-level validators on the FormArray itself.

Revision Notes

Key Takeaways

  • 1. FormArray holds dynamic lists of form controls
  • 2. push() adds controls, removeAt() removes them
  • 3. formArrayName + formGroupName bind array items in templates
  • 4. Validation can be per-item or array-level

Interview Tips

  • • Explain when to use FormArray vs a single FormGroup
  • • Be ready to describe the template binding pattern
  • • Mention array-level validators for minimum items

Cheat Sheet

FormArray Cheat Sheet

Create:

this.fb.array([this.createItem()])

Add/remove:

this.items.push(this.createItem());
this.items.removeAt(index);

Template:

<div formArrayName="items">
  @for (item of items.controls; track item; let i = $index) {
    <div [formGroupName]="i">
      <input formControlName="product">
    </div>
  }
</div>

Continue Learning