Signal Collections: viewChildren, contentChildren

Avanzado
Angular
Angular
Actualizado: 25/09/2025

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 - Autor del tutorial

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.