Sintaxis @for y track
La sintaxis @for de Angular representa una evolución significativa en la forma de iterar sobre colecciones en las plantillas. Esta nueva estructura de control reemplaza completamente a la directiva *ngFor, proporcionando mejor rendimiento y una sintaxis más intuitiva que se alinea con los patrones modernos de desarrollo.
Estructura básica de @for
La sintaxis fundamental de @for sigue un patrón declarativo que resulta familiar para desarrolladores que han trabajado con estructuras de control en otros lenguajes:
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
Este bloque @for requiere dos elementos esenciales: la expresión de iteración (item of items
) y la función track (track item.id
). La expresión de iteración define qué colección recorrer y cómo nombrar cada elemento, mientras que la función track optimiza el rendimiento del renderizado.
La importancia del tracking
El parámetro track no es opcional en @for, a diferencia de trackBy en *ngFor que era opcional. Esta decisión de diseño obliga a los desarrolladores a considerar el rendimiento desde el principio, evitando problemas comunes de renderizado innecesario.
// Componente con lista de usuarios
export class UserListComponent {
users = [
{ id: 1, name: 'Ana García', email: 'ana@email.com' },
{ id: 2, name: 'Carlos López', email: 'carlos@email.com' },
{ id: 3, name: 'María Rodríguez', email: 'maria@email.com' }
];
}
<!-- Template con @for optimizado -->
@for (user of users; track user.id) {
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
}
Estrategias de tracking
Angular ofrece múltiples estrategias de tracking según el tipo de datos que manejes:
Para objetos con identificadores únicos:
@for (product of products; track product.id) {
<div class="product">{{ product.name }} - {{ product.price }}€</div>
}
Para arrays de primitivos:
// Array de strings
categories = ['Tecnología', 'Deportes', 'Música', 'Cocina'];
@for (category of categories; track category) {
<span class="tag">{{ category }}</span>
}
Usando el índice como tracking:
@for (item of items; track $index) {
<li>Elemento {{ $index + 1 }}: {{ item }}</li>
}
Variables contextuales disponibles
La sintaxis @for proporciona variables contextuales útiles para crear interfaces más dinámicas:
@for (task of tasks; track task.id) {
<div class="task-item"
[class.first]="$first"
[class.last]="$last"
[class.even]="$even">
<span class="task-number">{{ $index + 1 }}</span>
<span class="task-content">{{ task.title }}</span>
@if ($first) {
<span class="badge">Primera tarea</span>
}
@if ($last) {
<span class="badge">Última tarea</span>
}
</div>
}
Las variables contextuales incluyen:
$index
- Índice actual del elemento (empezando en 0)$first
- True si es el primer elemento$last
- True si es el último elemento$even
- True si el índice es par$odd
- True si el índice es impar$count
- Número total de elementos en la colección
Tracking con funciones personalizadas
Para casos más complejos, puedes crear funciones de tracking personalizadas en el componente:
export class OrderListComponent {
orders = [
{ id: 'ORD-001', customer: 'Juan Pérez', total: 125.50 },
{ id: 'ORD-002', customer: 'Ana Silva', total: 89.99 }
];
// Función de tracking personalizada
trackByOrderId(index: number, order: any): string {
return order.id;
}
}
@for (order of orders; track trackByOrderId($index, order)) {
<div class="order-summary">
<strong>{{ order.id }}</strong> - {{ order.customer }}
<span class="total">{{ order.total }}€</span>
</div>
}
Consideraciones de rendimiento
El tracking correcto es fundamental para el rendimiento de Angular. Cuando la colección cambia, Angular utiliza la función track para determinar qué elementos del DOM pueden reutilizarse:
// ❌ Tracking incorrecto - recrea todos los elementos
@for (user of users; track $index) {
<div>{{ user.name }}</div>
}
// ✅ Tracking óptimo - reutiliza elementos existentes
@for (user of users; track user.id) {
<div>{{ user.name }}</div>
}
Con un tracking apropiado, Angular puede optimizar automáticamente las actualizaciones del DOM, manteniendo el estado de componentes anidados y evitando recálculos innecesarios de estilos y animaciones.
La sintaxis @for con tracking obligatorio representa un paso hacia adelante en la optimización por defecto, asegurando que las aplicaciones Angular mantengan un rendimiento consistente incluso con listas grandes y dinámicas.
Iteración de colecciones
La iteración de colecciones con @for va más allá de simples arrays, permitiendo trabajar con diversos tipos de estructuras de datos y situaciones comunes en aplicaciones Angular modernas. Esta flexibilidad hace que @for sea una herramienta versátil para renderizar contenido dinámico desde múltiples fuentes de datos.
Trabajando con diferentes tipos de colecciones
Angular @for puede iterar sobre cualquier estructura que implemente el protocolo iterable de JavaScript, lo que incluye arrays, strings, Maps, Sets y objetos transformados:
Iteración sobre arrays de objetos complejos:
export class ProductCatalogComponent {
products = [
{
id: 1,
name: 'Laptop Gaming',
price: 1299.99,
category: 'Tecnología',
inStock: true,
tags: ['gaming', 'portátil', 'alto rendimiento']
},
{
id: 2,
name: 'Auriculares Bluetooth',
price: 89.50,
category: 'Audio',
inStock: false,
tags: ['inalámbrico', 'música']
}
];
}
@for (product of products; track product.id) {
<article class="product-card">
<header>
<h3>{{ product.name }}</h3>
<span class="price">{{ product.price }}€</span>
</header>
<div class="product-info">
<span class="category">{{ product.category }}</span>
<span class="stock-status" [class.out-of-stock]="!product.inStock">
{{ product.inStock ? 'En stock' : 'Agotado' }}
</span>
</div>
<div class="tags">
@for (tag of product.tags; track tag) {
<span class="tag">{{ tag }}</span>
}
</div>
</article>
}
Iteración sobre Maps y Sets:
export class ConfigurationComponent {
// Map para configuraciones clave-valor
settings = new Map([
['theme', 'oscuro'],
['language', 'es'],
['notifications', 'activadas'],
['autoSave', 'habilitado']
]);
// Set para elementos únicos
availableLanguages = new Set(['es', 'en', 'fr', 'de', 'it']);
}
<!-- Iterando sobre Map entries -->
<div class="settings-panel">
<h3>Configuración actual</h3>
@for (setting of settings; track setting[0]) {
<div class="setting-item">
<span class="setting-key">{{ setting[0] }}</span>
<span class="setting-value">{{ setting[1] }}</span>
</div>
}
</div>
<!-- Iterando sobre Set -->
<div class="language-selector">
<h3>Idiomas disponibles</h3>
@for (language of availableLanguages; track language) {
<button class="language-btn" [value]="language">
{{ language.toUpperCase() }}
</button>
}
</div>
Manejo de colecciones vacías con @empty
Una característica destacada de @for es el bloque @empty, que proporciona una forma elegante de manejar colecciones vacías sin lógica condicional adicional:
export class NotificationListComponent {
notifications: Notification[] = [];
loadNotifications() {
// Simulación de carga de datos
setTimeout(() => {
this.notifications = [
{ id: 1, message: 'Nuevo mensaje recibido', type: 'info' },
{ id: 2, message: 'Actualización disponible', type: 'warning' }
];
}, 2000);
}
}
<div class="notifications-container">
<h2>Centro de notificaciones</h2>
@for (notification of notifications; track notification.id) {
<div class="notification" [class]="notification.type">
<span class="message">{{ notification.message }}</span>
<button class="dismiss-btn">×</button>
</div>
} @empty {
<div class="empty-state">
<p>No tienes notificaciones nuevas</p>
<button (click)="loadNotifications()" class="refresh-btn">
Actualizar
</button>
</div>
}
</div>
Iteración con transformaciones de datos
Muchas veces necesitas transformar o filtrar datos antes de mostrarlos. Puedes combinar @for con métodos del componente para crear vistas dinámicas:
export class TaskManagerComponent {
allTasks = [
{ id: 1, title: 'Revisar código', completed: false, priority: 'alta' },
{ id: 2, title: 'Actualizar documentación', completed: true, priority: 'media' },
{ id: 3, title: 'Preparar presentación', completed: false, priority: 'alta' },
{ id: 4, title: 'Responder emails', completed: true, priority: 'baja' }
];
currentFilter = 'all'; // 'all', 'pending', 'completed'
get filteredTasks() {
switch (this.currentFilter) {
case 'pending':
return this.allTasks.filter(task => !task.completed);
case 'completed':
return this.allTasks.filter(task => task.completed);
default:
return this.allTasks;
}
}
get tasksByPriority() {
return this.filteredTasks.reduce((groups, task) => {
const priority = task.priority;
if (!groups[priority]) {
groups[priority] = [];
}
groups[priority].push(task);
return groups;
}, {} as Record<string, typeof this.allTasks>);
}
}
<div class="task-manager">
<!-- Filtros -->
<div class="filters">
<button (click)="currentFilter = 'all'"
[class.active]="currentFilter === 'all'">Todas</button>
<button (click)="currentFilter = 'pending'"
[class.active]="currentFilter === 'pending'">Pendientes</button>
<button (click)="currentFilter = 'completed'"
[class.active]="currentFilter === 'completed'">Completadas</button>
</div>
<!-- Tareas agrupadas por prioridad -->
@for (priorityGroup of Object.entries(tasksByPriority); track priorityGroup[0]) {
<div class="priority-group">
<h3 class="priority-header priority-{{ priorityGroup[0] }}">
Prioridad {{ priorityGroup[0] }}
</h3>
@for (task of priorityGroup[1]; track task.id) {
<div class="task-item" [class.completed]="task.completed">
<input type="checkbox" [checked]="task.completed">
<span class="task-title">{{ task.title }}</span>
</div>
} @empty {
<p class="no-tasks">No hay tareas de prioridad {{ priorityGroup[0] }}</p>
}
</div>
}
</div>
Iteración anidada y casos complejos
Para estructuras de datos jerárquicas, @for maneja perfectamente la iteración anidada manteniendo el rendimiento:
export class MenuComponent {
menuStructure = [
{
section: 'Administración',
icon: 'admin',
items: [
{ name: 'Usuarios', route: '/admin/users', permissions: ['admin'] },
{ name: 'Configuración', route: '/admin/config', permissions: ['admin'] }
]
},
{
section: 'Contenido',
icon: 'content',
items: [
{ name: 'Artículos', route: '/content/articles', permissions: ['editor'] },
{ name: 'Medios', route: '/content/media', permissions: ['editor'] },
{ name: 'Comentarios', route: '/content/comments', permissions: ['moderator'] }
]
}
];
}
<nav class="sidebar-menu">
@for (section of menuStructure; track section.section) {
<div class="menu-section">
<div class="section-header">
<i class="icon-{{ section.icon }}"></i>
<span>{{ section.section }}</span>
</div>
<ul class="section-items">
@for (item of section.items; track item.route) {
<li class="menu-item">
<a [routerLink]="item.route" class="menu-link">
{{ item.name }}
</a>
<!-- Mostrar permisos como badges -->
<div class="permissions">
@for (permission of item.permissions; track permission) {
<span class="permission-badge">{{ permission }}</span>
}
</div>
</li>
} @empty {
<li class="no-items">No hay elementos disponibles</li>
}
</ul>
</div>
} @empty {
<div class="empty-menu">
<p>Menú no disponible</p>
</div>
}
</nav>
Optimización para colecciones grandes
Cuando trabajas con colecciones extensas, considera técnicas de optimización que complementen el tracking de @for:
export class DataTableComponent {
allRecords = []; // Array con miles de registros
pageSize = 50;
currentPage = 0;
get paginatedRecords() {
const start = this.currentPage * this.pageSize;
const end = start + this.pageSize;
return this.allRecords.slice(start, end);
}
get totalPages() {
return Math.ceil(this.allRecords.length / this.pageSize);
}
}
<div class="data-table">
<!-- Solo renderizar registros visibles -->
@for (record of paginatedRecords; track record.id) {
<div class="table-row">
<span class="cell">{{ record.name }}</span>
<span class="cell">{{ record.email }}</span>
<span class="cell">{{ record.department }}</span>
</div>
} @empty {
<div class="no-data">
<p>No se encontraron registros</p>
</div>
}
<!-- Controles de paginación -->
<div class="pagination">
<button [disabled]="currentPage === 0"
(click)="currentPage = currentPage - 1">
Anterior
</button>
<span>Página {{ currentPage + 1 }} de {{ totalPages }}</span>
<button [disabled]="currentPage === totalPages - 1"
(click)="currentPage = currentPage + 1">
Siguiente
</button>
</div>
</div>
La iteración de colecciones con @for proporciona una base sólida para crear interfaces dinámicas y eficientes, adaptándose a las necesidades específicas de cada aplicación mientras mantiene un código limpio y performante.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Angular
Documentación oficial de Angular
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Angular
Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender la sintaxis básica y estructura del bloque @for en Angular.
- Aprender la importancia y uso del parámetro track para optimizar el renderizado.
- Manejar diferentes tipos de colecciones (arrays, Maps, Sets) con @for.
- Utilizar variables contextuales para mejorar la interacción en las plantillas.
- Implementar iteración anidada, manejo de colecciones vacías y optimización para grandes volúmenes de datos.