viewChildren() y contentChildren()
Las funciones viewChildren() y contentChildren() representan la evolución natural de las queries individuales hacia el manejo de colecciones de elementos. Mientras que viewChild() y contentChild() nos permiten acceder a un único elemento, estas nuevas APIs nos proporcionan arrays reactivos que se actualizan automáticamente cuando se añaden o eliminan elementos del DOM.
Sintaxis y uso básico
viewChildren() permite obtener múltiples elementos hijos del template del componente:
import { Component, viewChildren } from '@angular/core';
import { ItemComponent } from './item.component';
@Component({
selector: 'app-list',
standalone: true,
imports: [ItemComponent],
template: `
@for (item of items; track item.id) {
<app-item [data]="item" />
}
`
})
export class ListComponent {
items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
];
// Signal que contiene array de componentes ItemComponent
itemComponents = viewChildren(ItemComponent);
ngAfterViewInit() {
// El signal se actualiza automáticamente
console.log('Componentes encontrados:', this.itemComponents().length);
}
}
contentChildren() funciona de manera similar pero para elementos proyectados mediante ng-content:
import { Component, contentChildren } from '@angular/core';
import { CardComponent } from './card.component';
@Component({
selector: 'app-container',
standalone: true,
template: `
<div class="container">
<ng-content></ng-content>
</div>
`
})
export class ContainerComponent {
// Signal que contiene array de componentes proyectados
cards = contentChildren(CardComponent);
ngAfterContentInit() {
console.log('Cards proyectadas:', this.cards().length);
}
}
Queries por selector y filtros
Al igual que las queries individuales, podemos usar selectores CSS o template reference variables:
@Component({
template: `
<button #btn class="primary">Botón 1</button>
<button #btn class="secondary">Botón 2</button>
<button class="primary">Botón 3</button>
<input #input type="text" />
<input #input type="email" />
`
})
export class ButtonGroupComponent {
// Por referencia de template
buttons = viewChildren<ElementRef<HTMLButtonElement>>('btn');
// Por selector CSS
primaryButtons = viewChildren('.primary');
// Por tipo de componente
inputs = viewChildren<ElementRef<HTMLInputElement>>('input');
logCounts() {
console.log('Botones con #btn:', this.buttons().length); // 2
console.log('Botones .primary:', this.primaryButtons().length); // 2
console.log('Inputs:', this.inputs().length); // 2
}
}
Reactividad automática
La característica más valiosa de estas signal collections es su capacidad de actualizarse automáticamente cuando el DOM cambia:
@Component({
selector: 'app-dynamic-list',
standalone: true,
imports: [ItemComponent],
template: `
<button (click)="addItem()">Añadir elemento</button>
<button (click)="removeItem()">Eliminar último</button>
@for (item of items; track item.id) {
<app-item [data]="item" />
}
<p>Total de componentes: {{ itemComponents().length }}</p>
`
})
export class DynamicListComponent {
items = signal([
{ id: 1, name: 'Item inicial' }
]);
itemComponents = viewChildren(ItemComponent);
// El signal se actualiza automáticamente cuando cambia el DOM
constructor() {
effect(() => {
console.log('Número de componentes cambió:', this.itemComponents().length);
});
}
addItem() {
const newId = this.items().length + 1;
this.items.update(items => [
...items,
{ id: newId, name: `Item ${newId}` }
]);
// itemComponents() se actualiza automáticamente
}
removeItem() {
this.items.update(items => items.slice(0, -1));
// itemComponents() se actualiza automáticamente
}
}
Opciones de configuración
Las signal collections admiten las mismas opciones que las queries individuales:
@Component({
template: `
<div #container>
<app-item></app-item>
<div>
<app-item></app-item>
</div>
</div>
`
})
export class ConfiguredQueriesComponent {
// Solo buscar en descendientes directos
directItems = viewChildren(ItemComponent, {
descendants: false
});
// Leer como ElementRef en lugar del componente
containers = viewChildren('container', {
read: ElementRef
});
}
Casos de uso prácticos
Las signal collections son especialmente útiles para coordinar múltiples elementos:
@Component({
selector: 'app-tab-group',
standalone: true,
imports: [TabComponent],
template: `
@for (tab of tabs; track tab.id) {
<app-tab
[title]="tab.title"
[active]="tab.id === activeTab()"
(tabClick)="selectTab(tab.id)" />
}
`
})
export class TabGroupComponent {
tabs = [
{ id: 1, title: 'Tab 1' },
{ id: 2, title: 'Tab 2' },
{ id: 3, title: 'Tab 3' }
];
activeTab = signal(1);
tabComponents = viewChildren(TabComponent);
selectTab(tabId: number) {
this.activeTab.set(tabId);
// Coordinar todos los componentes tab
this.tabComponents().forEach((tab, index) => {
if (this.tabs[index].id === tabId) {
tab.activate();
} else {
tab.deactivate();
}
});
}
// Navegar programáticamente entre tabs
nextTab() {
const currentIndex = this.tabs.findIndex(tab => tab.id === this.activeTab());
const nextIndex = (currentIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[nextIndex].id);
}
}
Las collections también simplifican operaciones batch sobre múltiples elementos:
@Component({
template: `
<app-form-field label="Nombre" required></app-form-field>
<app-form-field label="Email" required></app-form-field>
<app-form-field label="Teléfono"></app-form-field>
<button (click)="validateAll()">Validar todo</button>
<button (click)="resetAll()">Reset todo</button>
`
})
export class FormComponent {
formFields = viewChildren(FormFieldComponent);
validateAll() {
const isValid = this.formFields().every(field => field.validate());
console.log('Formulario válido:', isValid);
return isValid;
}
resetAll() {
this.formFields().forEach(field => field.reset());
}
getFormData() {
return this.formFields().reduce((data, field) => {
data[field.name] = field.value;
return data;
}, {} as Record<string, any>);
}
}
La principal ventaja de viewChildren() y contentChildren() sobre los decoradores tradicionales es que eliminan la necesidad de gestionar manualmente los cambios en las colecciones. El sistema de signals se encarga automáticamente de mantener sincronizadas estas colecciones reactivas con el estado real del DOM.
Colecciones reactivas de elementos
Las signal collections van más allá del simple acceso a elementos DOM: funcionan como verdaderos signals que se integran completamente con el sistema de reactividad de Angular. Esta integración permite crear patrones reactivos sofisticados donde los cambios en las colecciones de elementos desencadenan actualizaciones automáticas en toda la aplicación.
Integración con computed signals
Las colecciones pueden utilizarse como dependencias en computed signals, creando valores derivados que se recalculan automáticamente:
@Component({
template: `
@for (task of tasks(); track task.id) {
<app-task [task]="task" #taskRef />
}
<div class="stats">
<p>Tareas completadas: {{ completedCount() }}</p>
<p>Progreso: {{ progressPercentage() }}%</p>
<p>Tareas visibles: {{ visibleTasksCount() }}</p>
</div>
`
})
export class TaskListComponent {
tasks = signal([
{ id: 1, title: 'Tarea 1', completed: false },
{ id: 2, title: 'Tarea 2', completed: true },
{ id: 3, title: 'Tarea 3', completed: false }
]);
taskComponents = viewChildren('taskRef');
// Computed que reacciona a cambios en la colección
completedCount = computed(() => {
return this.taskComponents()
.filter(task => task.isCompleted())
.length;
});
progressPercentage = computed(() => {
const total = this.taskComponents().length;
if (total === 0) return 0;
return Math.round((this.completedCount() / total) * 100);
});
visibleTasksCount = computed(() => {
return this.taskComponents()
.filter(task => task.isVisible())
.length;
});
}
Efectos reactivos sobre colecciones
Los effects pueden reaccionar automáticamente a cambios en las colecciones, permitiendo sincronización con sistemas externos:
@Component({
selector: 'app-chart-container',
template: `
@for (dataPoint of chartData(); track dataPoint.id) {
<app-chart-point [data]="dataPoint" />
}
`
})
export class ChartContainerComponent {
chartData = signal([
{ id: 1, value: 10, label: 'A' },
{ id: 2, value: 20, label: 'B' }
]);
chartPoints = viewChildren(ChartPointComponent);
constructor() {
// Effect que se ejecuta cuando cambia la colección
effect(() => {
const points = this.chartPoints();
if (points.length > 0) {
// Sincronizar con librería de gráficos externa
this.updateExternalChart(points.map(p => p.getValue()));
// Aplicar animaciones coordinadas
this.animatePoints(points);
// Persistir estado en localStorage
this.saveChartState(points);
}
});
}
private updateExternalChart(values: number[]) {
// Integración con D3.js, Chart.js, etc.
console.log('Actualizando gráfico externo:', values);
}
private animatePoints(points: ChartPointComponent[]) {
points.forEach((point, index) => {
point.animateEntry(index * 100); // Delay escalonado
});
}
private saveChartState(points: ChartPointComponent[]) {
const state = points.map(p => p.serialize());
localStorage.setItem('chartState', JSON.stringify(state));
}
}
Reactividad bidireccional
Las colecciones pueden tanto reaccionar a cambios como desencadenar actualizaciones en otros signals:
@Component({
template: `
<input
type="text"
[value]="searchTerm()"
(input)="searchTerm.set($event.target.value)"
placeholder="Buscar elementos..." />
@for (item of filteredItems(); track item.id) {
<app-filterable-item [item]="item" />
}
<p>Elementos visibles: {{ visibleCount() }}</p>
`
})
export class FilterableListComponent {
searchTerm = signal('');
allItems = signal([
{ id: 1, name: 'Angular', category: 'framework' },
{ id: 2, name: 'React', category: 'framework' },
{ id: 3, name: 'TypeScript', category: 'language' }
]);
itemComponents = viewChildren(FilterableItemComponent);
// Computed que filtra elementos reactivamente
filteredItems = computed(() => {
const term = this.searchTerm().toLowerCase();
return this.allItems().filter(item =>
item.name.toLowerCase().includes(term) ||
item.category.toLowerCase().includes(term)
);
});
// Computed que cuenta elementos visibles en el DOM
visibleCount = computed(() => {
return this.itemComponents()
.filter(component => component.isVisible())
.length;
});
constructor() {
// Effect que coordina filtros y visibilidad
effect(() => {
const components = this.itemComponents();
const term = this.searchTerm();
components.forEach(component => {
const matches = component.matchesSearch(term);
component.setVisibility(matches);
if (matches) {
component.highlightMatches(term);
}
});
});
}
}
Composición de colecciones reactivas
Las múltiples colecciones pueden combinarse para crear comportamientos complejos:
@Component({
template: `
<div class="toolbar">
@for (tool of tools(); track tool.id) {
<app-tool [config]="tool" />
}
</div>
<div class="canvas">
@for (shape of shapes(); track shape.id) {
<app-shape [data]="shape" />
}
</div>
`
})
export class DrawingEditorComponent {
tools = signal([
{ id: 'select', name: 'Seleccionar', active: true },
{ id: 'draw', name: 'Dibujar', active: false }
]);
shapes = signal([
{ id: 1, type: 'circle', selected: false },
{ id: 2, type: 'rectangle', selected: true }
]);
toolComponents = viewChildren(ToolComponent);
shapeComponents = viewChildren(ShapeComponent);
// Computed que combina múltiples colecciones
editorState = computed(() => {
const activeTool = this.toolComponents()
.find(tool => tool.isActive());
const selectedShapes = this.shapeComponents()
.filter(shape => shape.isSelected());
return {
activeToolId: activeTool?.getId(),
selectedCount: selectedShapes.length,
canEdit: selectedShapes.length > 0
};
});
constructor() {
// Effect que coordina herramientas y formas
effect(() => {
const state = this.editorState();
const tools = this.toolComponents();
const shapes = this.shapeComponents();
// Aplicar modo de herramienta a todas las formas
shapes.forEach(shape => {
shape.setInteractionMode(state.activeToolId);
});
// Actualizar UI de herramientas según selección
tools.forEach(tool => {
tool.setEnabled(state.canEdit || tool.getId() === 'select');
});
});
}
}
Optimización de rendimiento reactivo
Las colecciones reactivas pueden optimizarse usando técnicas avanzadas de reactividad:
@Component({
template: `
@for (item of items(); track item.id) {
<app-performance-item [item]="item" />
}
`
})
export class OptimizedListComponent {
items = signal(Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
active: false
})));
itemComponents = viewChildren(PerformanceItemComponent);
// Computed con debounce implícito por el sistema de signals
activeItemsCount = computed(() => {
return this.itemComponents()
.filter(item => item.isActive())
.length;
});
// Effect que maneja actualizaciones masivas de forma eficiente
constructor() {
effect(() => {
const components = this.itemComponents();
// Usar requestAnimationFrame para operaciones pesadas
if (components.length > 0) {
this.scheduleUpdate(() => {
this.updateVisibleItems(components);
});
}
});
}
private scheduleUpdate(callback: () => void) {
requestAnimationFrame(() => {
callback();
});
}
private updateVisibleItems(components: PerformanceItemComponent[]) {
// Actualizar solo elementos visibles en viewport
const viewport = this.getViewportBounds();
components.forEach(component => {
const bounds = component.getBounds();
const isVisible = this.intersects(viewport, bounds);
if (isVisible) {
component.enableUpdates();
} else {
component.disableUpdates();
}
});
}
private getViewportBounds() {
return {
top: window.scrollY,
bottom: window.scrollY + window.innerHeight,
left: window.scrollX,
right: window.scrollX + window.innerWidth
};
}
private intersects(a: any, b: any): boolean {
return !(a.right < b.left ||
a.left > b.right ||
a.bottom < b.top ||
a.top > b.bottom);
}
}
La reactividad de estas colecciones permite crear aplicaciones que responden automáticamente a cambios en el DOM, manteniendo la sincronización entre el estado de la aplicación y los elementos visuales sin intervención manual. Este enfoque elimina la complejidad de gestionar manualmente los ciclos de vida y las actualizaciones de colecciones de elementos.

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 diferencia entre viewChildren() y contentChildren() y su uso para acceder a colecciones de elementos.
- Aprender a utilizar selectores CSS y referencias de template para queries reactivas.
- Entender la reactividad automática de las signal collections y su integración con computed y effects.
- Aplicar colecciones reactivas para coordinar múltiples componentes y operaciones batch.
- Optimizar el rendimiento y gestionar colecciones complejas mediante técnicas avanzadas de reactividad.