---
name: angular-moderno
description: Angular v17+/v20+. Signals, standalone components, OnPush, host bindings, nueva sintaxis de control flow (@if/@for/@switch), defer blocks y patrones modernos.
version: "1.0.1"
evolved: true
evolved-from: "1.0.0"
evolved-at: "2026-06-04"
evolved-by: "evolucionar"
evolved-note: "PE-009 patrón Angular 19+ ErrorHandler custom con provideBrowserGlobalErrorListeners (NO listeners manuales window.onerror). Origen: OIC v1.5 Slice 6 2026-06-04."
herramientasPermitidas: [Read]
exclusiones:
  - "No cargar para patrones Angular avanzados (zoneless, SSR, Resource API, interceptores funcionales) — para eso cargar `angular-avanzado`."
  - "No cargar para formularios Angular complejos, state management o animaciones avanzadas — cargar `angular-avanzado`."
  - "No cargar para frameworks React o Vue — los patrones de signals y control flow son específicos de Angular."
  - "No cargar para testing Angular (TestBed, harnesses, Spectator) — para testing cargar el skill de testing del stack."
evolvable: true  # default para skill estandar
---
# Angular Moderno (v17+)

## Cuándo NO cargar

- La tarea requiere Angular avanzado: zoneless, SSR/SSG con Angular Universal, Resource API, interceptores funcionales — cargar `angular-avanzado`.
- Los temas son formularios reactivos complejos, state management con signals store o animaciones Angular — cargar `angular-avanzado`.
- El framework es React, Vue o Svelte — los patrones de signals y `@if`/`@for` son específicos de Angular.
- La tarea es escribir tests Angular (TestBed, harnesses de Material) — usar el skill de testing correspondiente.

## Principios de la era moderna de Angular

Angular evolucionó hacia un modelo más reactivo y explícito:
- **Signals** reemplaza Zone.js para la detección de cambios.
- **Standalone** elimina NgModules como unidad de organización obligatoria.
- **New control flow** (@if, @for, @switch) reemplaza directivas estructurales.
- **OnPush + Signals** es el estándar para componentes de producción.

---

## Standalone Components

```typescript
import { Component, OnInit, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { FacturaCardComponent } from './factura-card/factura-card.component';

@Component({
  selector: 'app-facturas-lista',
  standalone: true,
  imports: [RouterLink, MatButtonModule, FacturaCardComponent],
  templateUrl: './facturas-lista.component.html',
  styleUrl: './facturas-lista.component.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FacturasListaComponent implements OnInit {
  private readonly facturasService = inject(FacturasService);
  ngOnInit(): void { ... }
}
```

---

## Signals — Estado reactivo

```typescript
import { signal, computed, Signal } from '@angular/core';

@Component({ ... })
export class FacturasListaComponent {
  private readonly _facturas = signal<Factura[]>([]);
  private readonly _cargando = signal(false);
  private readonly _filtro = signal('');

  readonly facturas: Signal<Factura[]> = this._facturas.asReadonly();
  readonly cargando: Signal<boolean> = this._cargando.asReadonly();

  // Derivaciones con computed() — NUNCA funciones directas en templates
  readonly facturasFiltradas = computed(() => {
    const filtro = this._filtro().toLowerCase();
    return this._facturas().filter(f =>
      f.folio.toLowerCase().includes(filtro)
    );
  });

  readonly totalFacturas = computed(() => this.facturasFiltradas().length);

  actualizarFiltro(valor: string): void {
    this._filtro.set(valor);
  }
}
```

---

## Nueva sintaxis de control flow

```html
<!-- @if — reemplaza *ngIf -->
@if (cargando()) {
  <mat-spinner diameter="40" />
} @else if (hayFacturas()) {
  <div class="lista-facturas">
    <!-- @for — reemplaza *ngFor, track OBLIGATORIO -->
    @for (factura of facturasFiltradas(); track factura.id) {
      <app-factura-card [factura]="factura" />
    } @empty {
      <p>No se encontraron facturas con ese filtro.</p>
    }
  </div>
} @else {
  <p>No tienes facturas registradas.</p>
}

<!-- @switch — reemplaza ngSwitch -->
@switch (factura.estatus) {
  @case ('emitida') { <span class="badge-verde">Emitida</span> }
  @case ('cancelada') { <span class="badge-rojo">Cancelada</span> }
}
```

---

## @defer — Carga diferida de componentes

```html
@defer (on viewport) {
  <app-grafica-ventas [datos]="datosGrafica()" />
} @placeholder {
  <div class="h-64 bg-gray-100 animate-pulse rounded"></div>
} @loading (minimum 300ms) {
  <mat-spinner />
} @error {
  <p>Error al cargar la gráfica.</p>
}
```

---

## Input y Output modernos (Angular v17.1+)

```typescript
import { input, output, model } from '@angular/core';

@Component({ ... })
export class FacturaCardComponent {
  readonly factura = input.required<Factura>();
  readonly modoCompacto = input(false);
  readonly facturaId = input('', { transform: (v: string) => v.trim() });

  readonly cancelar = output<string>();
  readonly seleccionar = output<Factura>();
  readonly checked = model(false);  // two-way binding

  alCancelar(): void {
    this.cancelar.emit(this.factura().id);
  }
}
```

---

## Host bindings, Services con Signals y Formularios reactivos

Para ejemplos completos de host bindings con clases dinámicas, services store con signals y HttpClient, y formularios reactivos con FormBuilder, ver [recursos/ejemplos-avanzados.md](recursos/ejemplos-avanzados.md).

---

## Anti-patrones a evitar

```typescript
// MAL: *ngIf / *ngFor en Angular v17+
// BIEN: @if / @for con track obligatorio

// MAL: funciones en templates (se evalúan en cada CD)
// {{ getTotal() }}
// BIEN: computed()
// readonly total = computed(() => this.calcularTotal());

// MAL: NgModules innecesarios en proyectos nuevos

// MAL: item?.propiedad para signals (NG8107)
// BIEN: @if (item()) { {{ item()!.nombre }} }
```

## Gotchas / Errores comunes no obvios

**`@for` sin `track` compila con advertencia en v17 pero causa re-renderizado completo de la lista**: si se omite `track` (o se usa `track $index` en listas mutables), Angular recrea todos los elementos DOM al cambiar el array en lugar de hacer reconciliación. Causa: sin `track`, Angular no puede identificar qué elemento cambió. Fix: usar `track item.id` con el identificador estable del elemento; solo usar `track $index` en listas de solo lectura que nunca cambian de orden.

**`computed()` que lee otra signal dentro de un `effect()` no re-ejecuta el efecto cuando cambia la computed**: si un `effect()` lee `this.total()` que es un `computed()`, el efecto sí se suscribe a la computed — pero si dentro del `effect` se llama a un método que modifica otra signal no leída directamente en el efecto, ese efecto no se re-ejecuta. Causa: Angular rastrea solo las signals leídas síncronamente dentro del cuerpo del efecto. Fix: leer explícitamente todas las signals de las que depende el efecto en su cuerpo, no mediante funciones intermedias.

**`input.required<T>()` accedido fuera del contexto de renderizado (en `constructor`) lanza error de runtime**: `this.factura()` dentro del `constructor` de un componente que usa `input.required<Factura>()` lanza `NG0950: Input is required but no value is available yet`. Causa: los inputs signal no tienen valor hasta que Angular completa la inicialización del componente. Fix: acceder a inputs en `ngOnInit`, en `computed()`, o en métodos del template — nunca en el constructor.

**`takeUntilDestroyed()` usado fuera del contexto de inyección lanza error**: `takeUntilDestroyed()` sin argumentos requiere un `DestroyRef` del contexto de inyección activo — si se llama dentro de un callback asíncrono (como `.then()` o `setTimeout`), el injection context ya no está activo. Causa: `inject()` solo funciona en contextos de inyección síncronos. Fix: capturar `DestroyRef` en el constructor con `private destroyRef = inject(DestroyRef)` y pasarlo explícitamente: `takeUntilDestroyed(this.destroyRef)`.

**Para captura global de errores runtime (window.onerror + unhandledrejection), usar `ErrorHandler` custom con `provideBrowserGlobalErrorListeners` — NO `window.addEventListener('error', ...)` manual** (Angular 19+; patrón portable OIC v1.5 2026-06-04): Angular 19+ provee `provideBrowserGlobalErrorListeners()` que ya registra los listeners nativos. Para reportar errores no controlados al backend (Sentry-style, audit handler, logs centralizados), proveer un `ErrorHandler` custom — listeners manuales duplican el registro y rompen la integración con el ciclo de DI/zone de Angular.

```typescript
// app.config.ts
import { ApplicationConfig, ErrorHandler, provideBrowserGlobalErrorListeners } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),                            // 1. Listeners nativos
    { provide: ErrorHandler, useClass: GlobalErrorHandler },         // 2. Tu handler custom
    // ...
  ],
};

// global-error-handler.service.ts
@Injectable({ providedIn: 'root' })
export class GlobalErrorHandler implements ErrorHandler {
  private readonly reporter = inject(ErrorReporterService);

  handleError(error: unknown): void {
    try {
      this.reporter.reportarErrorGlobal(error, 'global-error-handler');
    } catch {
      // Silenciar fallos del reporter — un handler global NUNCA debe propagar.
    }
    console.error(error);  // Preservar visibilidad en DevTools.
  }
}
```

**Causa**: con `provideBrowserGlobalErrorListeners` activo, Angular ya escucha `window.onerror`/`unhandledrejection` y los enruta a `ErrorHandler.handleError()`. Registrar listeners manuales propios duplica el callback (cada error se reporta dos veces) y se pierde la integración con el lifecycle Angular (zone tracking, DI context). **Fix**: usar EXCLUSIVAMENTE el `ErrorHandler` custom + `provideBrowserGlobalErrorListeners`.

**Reglas del handler**:
- `try/except` agresivo: el reporter NUNCA debe propagar al `handleError` (riesgo de recursión si el propio POST de reporte falla).
- Delegar a `console.error(error)` para preservar DevTools (no silenciar para devs).
- El reporter debe tener throttle propio (ej: 10 eventos/5s) y silenciar respuestas esperadas (401/403/404/429) — los detalles del reporter viven en su propio service, no en el `ErrorHandler`.
- Anti-bucle: el `ErrorReporterInterceptor` (si existe) debe excluir llamadas al propio endpoint POST de reportes.
