Capítulo 6 - Parte 1: Directivas estructurales built-in: ngIf, ngFor, ngSwitch¶
Parte 1 de 4 · Capítulo 6 · PARTE III - Templates y Directivas
Las directivas estructurales son un tipo especial de directiva que modifica la estructura del DOM: agregan, eliminan o reorganizan elementos. Se reconocen por el asterisco * que precede su nombre en el template. Aunque Angular 17 introdujo la nueva sintaxis @if/@for (→ Ver Capítulo 5, Parte 4), las directivas estructurales clásicas son esencia del lenguaje y siguen apareciendo en millones de proyectos activos. Entenderlas bien es indispensable para trabajar con bases de código existentes y para comprender qué hace el compilador detrás de la nueva sintaxis.
*ngIf y el secreto del asterisco¶
El asterisco en *ngIf no es decorativo: es una abreviatura que el compilador de Angular expande en sintaxis de <ng-template>. Estas dos formas son equivalentes:
<!-- Forma abreviada con asterisco -->
<p *ngIf="estaAutenticado">Bienvenido, usuario.</p>
<!-- Forma expandida que el compilador genera internamente -->
<ng-template [ngIf]="estaAutenticado">
<p>Bienvenido, usuario.</p>
</ng-template>
El compilador toma el elemento con *ngIf, lo envuelve en un <ng-template> y convierte la directiva estructural en un property binding normal. Esto explica la "microsyntax" de las directivas estructurales: es un mini-lenguaje que el compilador traduce a property bindings sobre <ng-template>.
Para usar *ngIf en un componente standalone, hay que importar NgIf o CommonModule:
import { Component } from '@angular/core';
import { NgIf } from '@angular/common'; // Importación específica (recomendada)
@Component({
selector: 'app-estado-sesion',
standalone: true,
imports: [NgIf],
template: `
<p *ngIf="estaAutenticado">Bienvenido al sistema.</p>
<p *ngIf="!estaAutenticado">Por favor inicia sesión.</p>
`
})
export class EstadoSesionComponent {
estaAutenticado = true;
}
*ngIf con then y else¶
La directiva *ngIf acepta los modificadores then y else que referencian <ng-template> con nombre. Es la forma clásica de manejar ramas condicionales alternativas:
import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-carga-datos',
standalone: true,
imports: [NgIf],
template: `
<!-- then y else apuntan a ng-template por su #nombre -->
<div *ngIf="!cargando; then contenido; else cargador"></div>
<ng-template #contenido>
<ul>
<li *ngFor="let item of datos">{{ item }}</li>
</ul>
</ng-template>
<ng-template #cargador>
<p>Cargando datos...</p>
</ng-template>
`
})
export class CargaDatosComponent {
cargando = false;
datos = ['Producto A', 'Producto B', 'Producto C'];
}
También existe la variante as que captura el valor para usarlo dentro del bloque, equivalente al ; as variable de la nueva sintaxis:
<!-- Captura el resultado de la expresión en la variable "usuario" -->
<div *ngIf="obtenerUsuario() as usuario">
<p>{{ usuario.nombre }}</p>
</div>
*ngFor con variables de contexto¶
*ngFor itera sobre cualquier iterable y crea una copia del elemento del template por cada ítem. La microsyntax expone varias variables locales que enriquecen la iteración:
import { Component } from '@angular/core';
import { NgFor } from '@angular/common';
interface Empleado {
id: number;
nombre: string;
departamento: string;
}
@Component({
selector: 'app-lista-empleados',
standalone: true,
imports: [NgFor],
template: `
<table>
<tbody>
<tr
*ngFor="
let empleado of empleados;
index as i;
first as esPrimero;
last as esUltimo;
even as esPar;
odd as esImpar;
trackBy: identificarEmpleado
"
[class.primera-fila]="esPrimero"
[class.ultima-fila]="esUltimo"
[class.fila-par]="esPar"
>
<td>{{ i + 1 }}</td>
<td>{{ empleado.nombre }}</td>
<td>{{ empleado.departamento }}</td>
</tr>
</tbody>
</table>
`
})
export class ListaEmpleadosComponent {
empleados: Empleado[] = [
{ id: 1, nombre: 'Ana Martínez', departamento: 'Ingeniería' },
{ id: 2, nombre: 'Carlos López', departamento: 'Diseño' },
{ id: 3, nombre: 'María Rodríguez', departamento: 'Marketing' },
];
// trackBy equivale al track de @for; recibe el índice y el elemento
identificarEmpleado(indice: number, empleado: Empleado): number {
return empleado.id;
}
}
Las variables de contexto se declaran con let variable of para el ítem principal y con variable as alias para los valores contextuales. trackBy recibe una función que devuelve el identificador único, con la misma función de optimización que track en la nueva sintaxis.
La microsyntax de las directivas estructurales¶
La microsyntax es el mini-lenguaje que Angular define para la expresión dentro de un *directiva. El compilador la parsea y la convierte en <ng-template> con property bindings. Para *ngFor la expansión completa es:
<!-- Microsyntax abreviada -->
<li *ngFor="let empleado of empleados; trackBy: identificarEmpleado">
<!-- Lo que el compilador genera -->
<ng-template
ngFor
let-empleado
[ngForOf]="empleados"
[ngForTrackBy]="identificarEmpleado"
>
<li>...</li>
</ng-template>
Cada palabra clave de la microsyntax se mapea a un property binding en el <ng-template>. Esto permite que las directivas estructurales sean altamente extensibles: la directiva declara qué palabras clave acepta y qué hacen.
*ngSwitch, ngSwitchCase y ngSwitchDefault¶
*ngSwitch es la variante de switch para templates. A diferencia de *ngIf y *ngFor, el switch se implementa con tres directivas que trabajan juntas:
import { Component } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
type Tema = 'claro' | 'oscuro' | 'sistema';
@Component({
selector: 'app-selector-tema',
standalone: true,
// Las tres directivas deben importarse por separado si no se usa CommonModule
imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
template: `
<!-- ngSwitch evalúa la expresión -->
<div [ngSwitch]="temaActual">
<!-- *ngSwitchCase compara con cada valor posible -->
<div *ngSwitchCase="'claro'">
<p>Tema claro activado. Fondo blanco, texto oscuro.</p>
</div>
<div *ngSwitchCase="'oscuro'">
<p>Tema oscuro activado. Fondo negro, texto claro.</p>
</div>
<div *ngSwitchCase="'sistema'">
<p>Siguiendo la preferencia del sistema operativo.</p>
</div>
<!-- *ngSwitchDefault cuando ningún case coincide -->
<div *ngSwitchDefault>
<p>Tema no reconocido. Usando tema claro por defecto.</p>
</div>
</div>
<div>
<button (click)="temaActual = 'claro'">Claro</button>
<button (click)="temaActual = 'oscuro'">Oscuro</button>
<button (click)="temaActual = 'sistema'">Sistema</button>
</div>
`
})
export class SelectorTemaComponent {
temaActual: Tema = 'claro';
}
Nótese que [ngSwitch] usa corchetes (property binding) mientras que *ngSwitchCase y *ngSwitchDefault usan asterisco (directivas estructurales). El contenedor recibe la expresión a evaluar y los hijos determinan qué se muestra.
Cuándo migrar a @if y @for¶
La migración a la nueva sintaxis es recomendable para proyectos en activo desarrollo, pero no es urgente para proyectos estables. Angular provee un schematic de migración automática:
Este comando convierte automáticamente *ngIf, *ngFor y *ngSwitch a la nueva sintaxis en todos los templates del proyecto. Las razones para migrar incluyen:
- Eliminación de las importaciones
NgIf,NgFor,NgSwitchen cada componente standalone. - Mejor análisis de tipos por parte del compilador (TypeScript puede inferir el tipo dentro del bloque
@if). - La obligatoriedad de
tracken@forevita errores de rendimiento por omisión. - El bloque
@emptysimplifica el patrón de lista vacía.
Para proyectos que usan NgModule en lugar de standalone, CommonModule sigue siendo la forma estándar de importar estas directivas, y la migración es opcional.
Diagrama: expansión de la microsyntax por el compilador¶
flowchart TD
M["Microsyntax en el template\n*ngFor=\"let x of lista; trackBy: fn\""]
M --> C["Compilador Angular"]
C --> T["<ng-template\n ngFor\n let-x\n [ngForOf]=\"lista\"\n [ngForTrackBy]=\"fn\"\n>"]
T --> R["Directiva NgFor\ninstanciada en tiempo de ejecución"]
R --> D["Nodos DOM creados\npara cada elemento"]
Puntos clave¶
- El asterisco
*en directivas estructurales es azúcar sintáctica para<ng-template>con property bindings. *ngIfaceptathen,elseyaspara manejar ramas y capturar valores.*ngForexponeindex,first,last,even,oddcomo variables de contexto;trackByoptimiza el rendimiento.*ngSwitchrequiere tres directivas coordinadas:[ngSwitch],*ngSwitchCasey*ngSwitchDefault.- En componentes standalone se importan individualmente (
NgIf,NgFor,NgSwitch) o juntos conCommonModule. - La migración a
@if/@fores automatizable conng generate @angular/core:control-flow.
¿Qué sigue?¶
En la Parte 2 estudiamos las directivas de atributo built-in: ngClass y ngStyle, que permiten manipular clases CSS y estilos de forma dinámica con una sintaxis más expresiva que los bindings directos [class] y [style].