CodeForge

Angular 21 de Cero a Experto / Formularios

Formularios dinámicos con FormArray

Teoría20 min20 XP

Hasta ahora tus formularios tenían campos fijos. Pero muchos casos reales necesitan un número VARIABLE de campos: agregar varios gastos de golpe, una lista de invitados que crece, ítems de una factura. Para eso está FormArray: un contenedor de controles que puedes agrandar y encoger en tiempo de ejecución. Hoy construyes formularios que crecen con el usuario.

La lista que crece

Crear y manipular un FormArray

varios-gastos.component.ts
import { Component, inject } from '@angular/core';
import { FormBuilder, FormArray, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
selector: 'app-varios-gastos',
imports: [ReactiveFormsModule],
template: `...`,
})
export class VariosGastosComponent {
private fb = inject(FormBuilder);

form = this.fb.group({
  gastos: this.fb.array([this.crearFila()]),   // arranca con una fila
});

// getter tipado para acceder al array cómodamente
get gastos(): FormArray {
  return this.form.controls.gastos;
}

private crearFila(): FormGroup {
  return this.fb.group({
    nombre: ['', Validators.required],
    valor: [0, [Validators.required, Validators.min(1)]],
  });
}

agregar() { this.gastos.push(this.crearFila()); }     // + una fila
quitar(i: number) { this.gastos.removeAt(i); }        // − la fila i
}

Las operaciones clave del FormArray: push(control) agrega al final, removeAt(i) elimina por índice, .at(i) accede a uno, .length cuenta, .clear() vacía. Cada fila es un FormGroup completo (con sus propios campos y validadores), así que validas cada gasto individualmente y el conjunto a la vez.

Renderizarlo en el template

Un FormArray se recorre con @for, usando formGroupName con el ÍNDICE para conectar cada fila:

template.ts
template: `
<form [formGroup]="form">
  <div formArrayName="gastos">
    @for (fila of gastos.controls; track $index) {
      <div [formGroupName]="$index">
        <input formControlName="nombre" placeholder="Nombre" />
        <input formControlName="valor" type="number" placeholder="Monto" />
        <button type="button" (click)="quitar($index)">Quitar</button>
      </div>
    }
  </div>
  <button type="button" (click)="agregar()">+ Agregar gasto</button>
  <button [disabled]="form.invalid">Guardar todos</button>
</form>
`,

El formArrayName="gastos" conecta el bloque con el array; dentro, @for sobre gastos.controls pinta una fila por control, y [formGroupName]="$index" enlaza cada fila con su FormGroup por posición. Al pulsar "+", agregar() hace push y aparece una fila nueva; al pulsar "Quitar", removeAt la elimina. El template reacciona solo.

La lógica de un FormArray, en vivo

El comportamiento de un FormArray —una colección que crece, se encoge, y cuya validez depende de todas sus filas— es lógica pura. Aquí, un mini-FormArray funcionando:

Observa cómo valid depende de TODAS las filas: una sola fila inválida vuelve inválido el array entero —igual que en Angular—. push y removeAt cambian el tamaño en runtime, y total es un derivado de las filas actuales. Ese es el modelo mental del FormArray.

¿Cuándo necesitas un `FormArray` en lugar de un `FormGroup`?

Mini-reto

En el playground, amplía el ArrayDeGastos: 1) un método clear() que vacíe todas las filas; 2) un getter errores que devuelva un array con { indice, error } de cada fila inválida; 3) un método insertAt(i, fila). Prueba agregar, quitar e insertar. Luego escribe cómo se vería el @for de Angular que renderiza este array con formGroupName.

Qué sigue

Cierras la teoría de formularios: reactivos tipados, validación en tres capas, errores accesibles y arrays dinámicos. Es hora del reto del módulo: construir el formulario de "nuevo gasto" completo —validado, accesible y con la lógica probada en vivo—. La puerta de datos de tu app, hecha con estándar profesional.