Template queries ViewChild y ContentChild

Avanzado
Angular
Angular
Actualizado: 24/09/2025

ViewChild Queries: @ViewChild, @ViewChildren

Las template queries nos permiten acceder desde el código TypeScript a elementos del DOM o componentes hijos definidos en nuestro template. En lecciones anteriores aprendiste a usar template reference variables con #ref para referenciar elementos desde el template. Ahora veremos cómo acceder a esos mismos elementos desde el código del componente usando decoradores especializados.

Los decoradores @ViewChild y @ViewChildren nos proporcionan una forma elegante de obtener referencias a elementos de la vista del componente, ya sean elementos HTML nativos, otros componentes o directivas.

Acceso a elementos con @ViewChild

El decorador @ViewChild nos permite obtener una referencia única a un elemento del template. Puede buscar elementos por diferentes criterios:

Búsqueda por template reference variable:

import { Component, ViewChild, ElementRef } from '@angular/core';

@Component({
  template: `
    <input #nombreInput type="text" placeholder="Escribe tu nombre">
    <button (click)="enfocarInput()">Enfocar input</button>
    <p>Valor actual: {{ obtenerValor() }}</p>
  `
})
export class MiComponente {
  @ViewChild('nombreInput') inputElement!: ElementRef<HTMLInputElement>;

  enfocarInput() {
    this.inputElement.nativeElement.focus();
  }

  obtenerValor() {
    return this.inputElement?.nativeElement.value || '';
  }
}

Búsqueda por tipo de componente:

import { Component, ViewChild } from '@angular/core';

// Componente hijo
@Component({
  selector: 'hijo-componente',
  template: '<p>Soy el componente hijo</p>'
})
export class HijoComponente {
  mensaje = 'Hola desde el hijo';
  
  saludar() {
    console.log('¡Saludo desde el componente hijo!');
  }
}

// Componente padre
@Component({
  template: `
    <hijo-componente></hijo-componente>
    <button (click)="interactuarConHijo()">Llamar método del hijo</button>
  `,
  imports: [HijoComponente]
})
export class PadreComponente {
  @ViewChild(HijoComponente) componenteHijo!: HijoComponente;

  interactuarConHijo() {
    console.log(this.componenteHijo.mensaje);
    this.componenteHijo.saludar();
  }
}

Múltiples elementos con @ViewChildren

Cuando necesitamos acceder a múltiples elementos que coinciden con nuestro criterio de búsqueda, utilizamos @ViewChildren. Este decorador devuelve un QueryList que contiene todas las referencias encontradas.

import { Component, ViewChildren, QueryList, ElementRef } from '@angular/core';

@Component({
  template: `
    <div>
      <input #item type="text" value="Elemento 1">
      <input #item type="text" value="Elemento 2">  
      <input #item type="text" value="Elemento 3">
    </div>
    <button (click)="procesarTodosLosInputs()">Procesar todos</button>
    <button (click)="limpiarTodos()">Limpiar todos</button>
  `
})
export class ListaComponente {
  @ViewChildren('item') inputs!: QueryList<ElementRef<HTMLInputElement>>;

  procesarTodosLosInputs() {
    this.inputs.forEach((input, index) => {
      console.log(`Input ${index + 1}: ${input.nativeElement.value}`);
    });
  }

  limpiarTodos() {
    this.inputs.forEach(input => {
      input.nativeElement.value = '';
    });
  }
}

Configuración de ViewChild

Los decoradores ViewChild admiten varias opciones de configuración que controlan cuándo y cómo se resuelven las consultas:

Consultas estáticas vs dinámicas:

@Component({
  template: `
    @if (mostrarInput) {
      <input #dinamicoInput type="text">
    }
    <input #estaticoInput type="text">
  `
})
export class ConfiguracionComponente {
  mostrarInput = false;

  // static: true - Se resuelve en ngOnInit (solo para elementos siempre presentes)
  @ViewChild('estaticoInput', { static: true }) 
  inputEstatico!: ElementRef;

  // static: false - Se resuelve en ngAfterViewInit (por defecto)
  @ViewChild('dinamicoInput', { static: false }) 
  inputDinamico!: ElementRef;

  ngOnInit() {
    // inputEstatico ya está disponible
    console.log('Input estático:', this.inputEstatico.nativeElement);
  }

  ngAfterViewInit() {
    // inputDinamico estará disponible si mostrarInput es true
    if (this.inputDinamico) {
      console.log('Input dinámico:', this.inputDinamico.nativeElement);
    }
  }
}

Trabajando con QueryList

El QueryList que devuelve @ViewChildren es una estructura especial que proporciona métodos útiles para trabajar con colecciones de elementos:

@Component({
  template: `
    @for (item of elementos; track item.id) {
      <div #tarjeta class="tarjeta">{{ item.nombre }}</div>
    }
    <button (click)="analizarTarjetas()">Analizar tarjetas</button>
  `
})
export class TarjetasComponente {
  elementos = [
    { id: 1, nombre: 'Tarjeta 1' },
    { id: 2, nombre: 'Tarjeta 2' },
    { id: 3, nombre: 'Tarjeta 3' }
  ];

  @ViewChildren('tarjeta') tarjetas!: QueryList<ElementRef>;

  analizarTarjetas() {
    console.log('Total de tarjetas:', this.tarjetas.length);
    console.log('Primera tarjeta:', this.tarjetas.first?.nativeElement.textContent);
    console.log('Última tarjeta:', this.tarjetas.last?.nativeElement.textContent);

    // Convertir a array para usar métodos de array
    const arrayTarjetas = this.tarjetas.toArray();
    arrayTarjetas.forEach((tarjeta, index) => {
      tarjeta.nativeElement.style.backgroundColor = `hsl(${index * 60}, 70%, 80%)`;
    });
  }

  ngAfterViewInit() {
    // Detectar cambios en la lista
    this.tarjetas.changes.subscribe(() => {
      console.log('Las tarjetas han cambiado');
    });
  }
}

Cuándo usar ViewChild vs ViewChildren

La elección entre @ViewChild y @ViewChildren depende de cuántos elementos esperas encontrar:

  • @ViewChild: Utilízalo cuando necesites acceder a un elemento específico. Si hay múltiples elementos que coinciden, obtendrás el primero.

  • @ViewChildren: Utilízalo cuando necesites acceder a todos los elementos que coinciden con el criterio de búsqueda.

Timing y ciclo de vida

Es importante entender cuándo están disponibles las referencias de ViewChild:

@Component({
  template: '<input #miInput type="text">'
})
export class TimingComponente {
  @ViewChild('miInput') input!: ElementRef;

  ngOnInit() {
    // ❌ input todavía no está disponible aquí
    console.log(this.input); // undefined
  }

  ngAfterViewInit() {
    // ✅ input ya está disponible aquí
    console.log(this.input); // ElementRef
    this.input.nativeElement.focus();
  }
}

Las template queries tradicionales que acabas de aprender son muy útiles para casos de uso específicos, pero Angular también ofrece Signal Queries como alternativa moderna que veremos más adelante en el módulo de Signals. Por ahora, dominar @ViewChild y @ViewChildren te dará las herramientas necesarias para interactuar con elementos de tu vista desde el código del componente.

ContentChild Queries: @ContentChild, @ContentChildren

Mientras que @ViewChild y @ViewChildren nos permiten acceder a elementos definidos en el template del propio componente, los decoradores @ContentChild y @ContentChildren nos dan acceso a elementos proyectados desde el componente padre mediante ng-content.

Estos decoradores son especialmente útiles cuando creamos componentes reutilizables que actúan como contenedores y necesitamos interactuar con el contenido que se proyecta en ellos.

Diferencia entre View y Content

Para entender mejor la diferencia, veamos un ejemplo visual:

// Componente contenedor
@Component({
  selector: 'mi-tarjeta',
  template: `
    <div class="tarjeta">
      <header class="cabecera">
        <!-- Este input pertenece a la VIEW del componente -->
        <input #viewInput type="text" placeholder="Título">
      </header>
      <main class="contenido">
        <!-- Este ng-content proyecta CONTENT desde el padre -->
        <ng-content></ng-content>
      </main>
    </div>
  `
})
export class TarjetaComponente {
  // Accede a elementos del template del componente
  @ViewChild('viewInput') inputDelTemplate!: ElementRef;
  
  // Accede a elementos proyectados desde el padre
  @ContentChild('contentInput') inputProyectado!: ElementRef;
}

// Uso del componente
@Component({
  template: `
    <mi-tarjeta>
      <!-- Este input será proyectado como CONTENT -->
      <input #contentInput type="text" placeholder="Contenido proyectado">
      <p>Este párrafo también es contenido proyectado</p>
    </mi-tarjeta>
  `,
  imports: [TarjetaComponente]
})
export class AppComponent {
}

Acceso a contenido proyectado con @ContentChild

El decorador @ContentChild nos permite obtener una referencia única a un elemento proyectado a través de ng-content:

@Component({
  selector: 'panel-expandible',
  template: `
    <div class="panel">
      <button (click)="alternarContenido()" class="boton-expandir">
        {{ expandido ? 'Contraer' : 'Expandir' }}
      </button>
      
      @if (expandido) {
        <div class="contenido-panel">
          <ng-content></ng-content>
        </div>
      }
    </div>
  `,
  styles: [`
    .contenido-panel { 
      border: 1px solid #ccc; 
      padding: 16px; 
      margin-top: 8px; 
    }
  `]
})
export class PanelExpandibleComponente {
  expandido = false;

  // Accede al botón proyectado desde el padre
  @ContentChild('botonAccion') botonAccion!: ElementRef<HTMLButtonElement>;

  alternarContenido() {
    this.expandido = !this.expandido;
    
    // Interactuar con el contenido proyectado
    if (this.expandido && this.botonAccion) {
      this.botonAccion.nativeElement.style.backgroundColor = '#4CAF50';
    }
  }

  ngAfterContentInit() {
    // Las content queries están disponibles aquí
    if (this.botonAccion) {
      console.log('Botón proyectado encontrado:', this.botonAccion.nativeElement);
    }
  }
}

Uso del componente:

@Component({
  template: `
    <panel-expandible>
      <h3>Contenido del panel</h3>
      <p>Este contenido se proyecta dentro del panel.</p>
      <button #botonAccion (click)="accionPersonalizada()">
        Acción personalizada
      </button>
    </panel-expandible>
  `,
  imports: [PanelExpandibleComponente]
})
export class ContenedorComponent {
  accionPersonalizada() {
    console.log('Acción ejecutada desde el contenido proyectado');
  }
}

Múltiples elementos proyectados con @ContentChildren

Cuando el contenido proyectado incluye múltiples elementos que necesitamos manejar, utilizamos @ContentChildren:

@Component({
  selector: 'lista-pestañas',
  template: `
    <div class="pestanas">
      @for (pestaña of pestañas; track $index) {
        <button 
          (click)="seleccionarPestaña($index)"
          [class.activa]="pestañaActiva === $index">
          Pestaña {{ $index + 1 }}
        </button>
      }
    </div>
    
    <div class="contenido-pestañas">
      <ng-content></ng-content>
    </div>
  `,
  styles: [`
    .pestanas button.activa { 
      background-color: #2196F3; 
      color: white; 
    }
  `]
})
export class ListaPestañasComponente {
  pestañaActiva = 0;
  pestañas: any[] = [];

  // Accede a todos los elementos de pestaña proyectados
  @ContentChildren('pestaña') elementosPestañas!: QueryList<ElementRef>;

  seleccionarPestaña(indice: number) {
    this.pestañaActiva = indice;
    this.actualizarVisibilidadPestañas();
  }

  actualizarVisibilidadPestañas() {
    this.elementosPestañas.forEach((elemento, indice) => {
      const divPestaña = elemento.nativeElement;
      divPestaña.style.display = indice === this.pestañaActiva ? 'block' : 'none';
    });
  }

  ngAfterContentInit() {
    // Inicializar pestañas basándose en el contenido proyectado
    this.pestañas = this.elementosPestañas.toArray();
    this.actualizarVisibilidadPestañas();

    // Detectar cambios en el contenido proyectado
    this.elementosPestañas.changes.subscribe(() => {
      this.pestañas = this.elementosPestañas.toArray();
      this.actualizarVisibilidadPestañas();
    });
  }
}

Uso con múltiples pestañas:

@Component({
  template: `
    <lista-pestañas>
      <div #pestaña>
        <h3>Primera pestaña</h3>
        <p>Contenido de la primera pestaña.</p>
      </div>
      
      <div #pestaña>
        <h3>Segunda pestaña</h3>
        <p>Contenido de la segunda pestaña.</p>
        <ul>
          <li>Elemento 1</li>
          <li>Elemento 2</li>
        </ul>
      </div>
      
      <div #pestaña>
        <h3>Tercera pestaña</h3>
        <p>Contenido de la tercera pestaña.</p>
      </div>
    </lista-pestañas>
  `,
  imports: [ListaPestañasComponente]
})
export class PaginaPestañasComponent {
}

Búsqueda por tipo de componente

Al igual que con ViewChild, también podemos buscar componentes proyectados por su tipo:

// Componente que será proyectado
@Component({
  selector: 'elemento-especial',
  template: `
    <div class="elemento-especial">
      <strong>{{ titulo }}</strong>
      <p>{{ contenido }}</p>
    </div>
  `
})
export class ElementoEspecialComponente {
  titulo = 'Elemento especial';
  contenido = 'Este es un componente proyectado';

  destacar() {
    console.log('Elemento destacado');
  }
}

// Componente contenedor
@Component({
  selector: 'galeria-elementos',
  template: `
    <div class="galeria">
      <button (click)="destacarElementos()">Destacar elementos especiales</button>
      <ng-content></ng-content>
    </div>
  `
})
export class GaleriaElementosComponente {
  // Buscar un componente específico proyectado
  @ContentChild(ElementoEspecialComponente) 
  elementoEspecial!: ElementoEspecialComponente;

  // Buscar todos los componentes de este tipo
  @ContentChildren(ElementoEspecialComponente) 
  todosElementosEspeciales!: QueryList<ElementoEspecialComponente>;

  destacarElementos() {
    this.todosElementosEspeciales.forEach(elemento => {
      elemento.destacar();
    });
  }

  ngAfterContentInit() {
    console.log('Elementos especiales encontrados:', this.todosElementosEspeciales.length);
  }
}

Ciclo de vida de ContentChild

Las content queries se resuelven en un momento diferente del ciclo de vida que las view queries:

@Component({
  selector: 'contenedor-con-ciclo',
  template: `
    <div class="contenedor">
      <ng-content></ng-content>
    </div>
  `
})
export class ContenedorConCicloComponente {
  @ContentChild('contenidoProyectado') contenido!: ElementRef;

  ngOnInit() {
    // ❌ contenido todavía no está disponible
    console.log('OnInit - Contenido:', this.contenido); // undefined
  }

  ngAfterContentInit() {
    // ✅ contenido ya está disponible aquí
    console.log('AfterContentInit - Contenido:', this.contenido);
  }

  ngAfterViewInit() {
    // ✅ contenido también está disponible aquí
    console.log('AfterViewInit - Contenido:', this.contenido);
  }
}

Cuándo usar ContentChild

Los decoradores @ContentChild y @ContentChildren son especialmente útiles para:

  • Componentes de layout que necesitan interactuar con su contenido proyectado
  • Componentes wrapper que añaden funcionalidad al contenido proyectado
  • Componentes contenedor como modales, pestañas o acordeones
  • Validación o procesamiento del contenido proyectado

Estas template queries tradicionales te permiten crear componentes muy flexibles que pueden interactuar tanto con su propia vista como con el contenido que reciben del exterior. En módulos posteriores veremos las Signal Queries como alternativa moderna, pero dominar @ContentChild y @ContentChildren es fundamental para trabajar con proyección de contenido en Angular.

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 el uso de @ViewChild y @ViewChildren para acceder a elementos y componentes en la vista del propio componente.
  • Aprender a configurar consultas estáticas y dinámicas con ViewChild.
  • Entender cómo trabajar con QueryList para manejar múltiples elementos.
  • Diferenciar entre ViewChild/ViewChildren y ContentChild/ContentChildren para acceder a contenido proyectado.
  • Conocer el ciclo de vida y el momento adecuado para acceder a las referencias obtenidas con estas queries.