viewChild() y contentChild()
Las signal queries representan una evolución natural de las queries tradicionales de Angular, ofreciendo una alternativa moderna y reactiva a los decoradores @ViewChild
y @ContentChild
. Estas nuevas funciones devuelven signals que se actualizan automáticamente cuando los elementos referenciados cambian, eliminando la complejidad de los lifecycle hooks tradicionales.
Sintaxis básica de viewChild()
La función viewChild()
permite obtener referencias a elementos del DOM o componentes hijos dentro del template del componente actual. Su sintaxis es considerablemente más simple que su contraparte decoradora:
import { Component, viewChild } from '@angular/core';
@Component({
selector: 'app-ejemplo',
standalone: true,
template: `
<input #nombreInput type="text" placeholder="Introduce tu nombre">
<button (click)="focusInput()">Enfocar input</button>
`
})
export class EjemploComponent {
// Signal query para el input
nombreInput = viewChild<ElementRef<HTMLInputElement>>('nombreInput');
focusInput() {
const input = this.nombreInput();
if (input) {
input.nativeElement.focus();
}
}
}
La ventaja inmediata es que no necesitamos ngAfterViewInit
para acceder al elemento. El signal se actualiza automáticamente cuando el elemento está disponible.
Queries obligatorias y opcionales
Por defecto, viewChild()
devuelve un signal que puede contener undefined
si el elemento no se encuentra. Para elementos que sabemos que siempre existirán, podemos usar viewChild.required()
:
@Component({
selector: 'app-contador',
standalone: true,
template: `
<div #contadorDisplay class="contador">{{ contador() }}</div>
<button (click)="incrementar()">+1</button>
`
})
export class ContadorComponent {
contador = signal(0);
// Signal query obligatoria - nunca será undefined
display = viewChild.required<ElementRef>('contadorDisplay');
incrementar() {
this.contador.update(val => val + 1);
// Podemos usar directamente sin verificar null
this.display().nativeElement.classList.add('actualizado');
}
}
contentChild() para contenido proyectado
La función contentChild()
funciona de manera similar pero busca dentro del contenido proyectado con <ng-content>
. Es especialmente útil para componentes que reciben contenido desde el padre:
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[slot=header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {
// Busca en el contenido proyectado
headerContent = contentChild<ElementRef>('[slot=header]');
ngAfterContentInit() {
const header = this.headerContent();
if (header) {
console.log('Header proyectado encontrado:', header.nativeElement.textContent);
}
}
}
Uso del componente:
@Component({
template: `
<app-card>
<h2 slot="header">Mi título</h2>
<p>Contenido de la tarjeta</p>
</app-card>
`
})
export class ParentComponent {}
Búsqueda por tipo de componente
Además de buscar por template reference variables, podemos buscar directamente por tipo de componente:
@Component({
selector: 'app-usuario',
standalone: true,
template: `
<h3>{{ nombre }}</h3>
<button (click)="editar()">Editar</button>
`
})
export class UsuarioComponent {
@Input() nombre = '';
editar() {
console.log('Editando usuario:', this.nombre);
}
}
@Component({
selector: 'app-lista',
standalone: true,
imports: [UsuarioComponent],
template: `
<app-usuario nombre="Ana"></app-usuario>
<app-usuario nombre="Carlos"></app-usuario>
<button (click)="editarPrimero()">Editar primero</button>
`
})
export class ListaComponent {
// Busca el primer componente de tipo UsuarioComponent
primerUsuario = viewChild(UsuarioComponent);
editarPrimero() {
const usuario = this.primerUsuario();
if (usuario) {
usuario.editar();
}
}
}
Reactividad automática con effects
Una de las ventajas más significativas de las signal queries es su integración natural con effects. Podemos reaccionar automáticamente cuando las referencias cambian:
@Component({
selector: 'app-modal',
standalone: true,
template: `
@if (mostrar()) {
<div #modalElement class="modal">
<h2>Modal dinámico</h2>
<button (click)="cerrar()">Cerrar</button>
</div>
}
<button (click)="abrir()">Abrir modal</button>
`
})
export class ModalComponent {
mostrar = signal(false);
modalRef = viewChild<ElementRef>('modalElement');
constructor() {
// Effect que se ejecuta cuando modalRef cambia
effect(() => {
const modal = this.modalRef();
if (modal) {
// Enfocar automáticamente cuando el modal aparece
modal.nativeElement.focus();
console.log('Modal montado en el DOM');
}
});
}
abrir() {
this.mostrar.set(true);
}
cerrar() {
this.mostrar.set(false);
}
}
Diferencias con @ViewChild y @ContentChild
Las signal queries ofrecen varias mejoras respecto a los decoradores tradicionales:
- 1. Reactividad automática: No necesitamos lifecycle hooks para detectar cambios en las referencias
- 2. Integración con signals: Se combinan naturalmente con computed signals y effects
- 3. Sintaxis más limpia: Menos boilerplate y configuración
- 4. Type safety mejorada: TypeScript puede inferir mejor los tipos
- 5. Mejor debugging: Los signals son más fáciles de debuggear que las propiedades decoradas
Comparación práctica:
// Forma tradicional con decoradores
export class ComponenteTradicional {
@ViewChild('elemento') elemento!: ElementRef;
ngAfterViewInit() {
// Solo disponible después de este lifecycle hook
this.elemento.nativeElement.focus();
}
}
// Forma moderna con signal queries
export class ComponenteModerno {
elemento = viewChild<ElementRef>('elemento');
constructor() {
// Disponible reactivamente
effect(() => {
const el = this.elemento();
if (el) {
el.nativeElement.focus();
}
});
}
}
Las signal queries representan la evolución natural hacia un Angular más reactivo y declarativo, donde las referencias a elementos se comportan como cualquier otro estado reactivo de la aplicación.
Queries reactivas con signals
Las signal queries transforman las referencias a elementos en primitivos reactivos que se integran completamente con el ecosistema de signals de Angular. Esta integración permite crear patrones reactivos sofisticados que responden automáticamente a cambios en el DOM y el estado de los componentes.
Computed signals derivados de queries
Los computed signals pueden derivar valores directamente de signal queries, creando cálculos que se actualizan automáticamente cuando las referencias cambian:
@Component({
selector: 'app-formulario',
standalone: true,
template: `
<form #formulario>
<input name="email" type="email" required>
<input name="password" type="password" required>
<button [disabled]="!esFormularioValido()">Enviar</button>
</form>
<div class="estado">
Estado: {{ estadoFormulario() }}
</div>
`
})
export class FormularioComponent {
formularioRef = viewChild<ElementRef<HTMLFormElement>>('formulario');
// Computed signal que deriva del estado del formulario
esFormularioValido = computed(() => {
const form = this.formularioRef();
return form ? form.nativeElement.checkValidity() : false;
});
// Computed signal para mostrar el estado
estadoFormulario = computed(() => {
const form = this.formularioRef();
if (!form) return 'Cargando...';
const elemento = form.nativeElement;
if (elemento.checkValidity()) {
return 'Válido ✓';
}
return `Inválido: ${elemento.validationMessage}`;
});
}
Reactividad en cadena con múltiples queries
Las signal queries permiten crear cadenas reactivas complejas donde el cambio de una query puede afectar a otras:
@Component({
selector: 'app-editor',
standalone: true,
template: `
<div class="editor">
<textarea #contenido (input)="actualizarContenido()">
{{ textoInicial }}
</textarea>
<div class="estadisticas" #stats>
Palabras: {{ estadisticas().palabras }}
Caracteres: {{ estadisticas().caracteres }}
</div>
<div #preview class="preview">
{{ contenidoProcesado() }}
</div>
</div>
`
})
export class EditorComponent {
textoInicial = 'Escribe tu contenido aquí...';
contenidoRef = viewChild<ElementRef<HTMLTextAreaElement>>('contenido');
statsRef = viewChild<ElementRef>('stats');
previewRef = viewChild<ElementRef>('preview');
// Signal para el contenido actual
contenidoActual = signal('');
// Computed signal para estadísticas
estadisticas = computed(() => {
const texto = this.contenidoActual();
return {
palabras: texto.trim() ? texto.trim().split(/\s+/).length : 0,
caracteres: texto.length
};
});
// Computed signal para contenido procesado
contenidoProcesado = computed(() => {
const texto = this.contenidoActual();
return texto.replace(/\n/g, '<br>');
});
constructor() {
// Effect que sincroniza cambios visuales
effect(() => {
const stats = this.estadisticas();
const statsElement = this.statsRef();
if (statsElement) {
// Animación cuando cambian las estadísticas
statsElement.nativeElement.classList.add('actualizado');
setTimeout(() => {
statsElement.nativeElement.classList.remove('actualizado');
}, 300);
}
});
// Effect para actualizar preview
effect(() => {
const contenido = this.contenidoProcesado();
const preview = this.previewRef();
if (preview) {
preview.nativeElement.innerHTML = contenido;
}
});
}
actualizarContenido() {
const textarea = this.contenidoRef();
if (textarea) {
this.contenidoActual.set(textarea.nativeElement.value);
}
}
}
Observación reactiva de cambios en el DOM
Las signal queries pueden combinarse con effects para observar cambios específicos en elementos del DOM:
@Component({
selector: 'app-lista-dinamica',
standalone: true,
template: `
<div class="controles">
<button (click)="agregarItem()">Agregar</button>
<button (click)="limpiar()">Limpiar</button>
</div>
<ul #lista class="items">
@for (item of items(); track item.id) {
<li class="item">{{ item.nombre }}</li>
}
</ul>
<div class="info">
Altura total: {{ alturaLista() }}px
Elementos visibles: {{ elementosVisibles() }}
</div>
`
})
export class ListaDinamicaComponent {
items = signal([
{ id: 1, nombre: 'Elemento 1' },
{ id: 2, nombre: 'Elemento 2' }
]);
listaRef = viewChild<ElementRef<HTMLUListElement>>('lista');
// Computed signal para altura de la lista
alturaLista = computed(() => {
const lista = this.listaRef();
return lista ? lista.nativeElement.scrollHeight : 0;
});
// Computed signal para elementos visibles
elementosVisibles = computed(() => {
const lista = this.listaRef();
if (!lista) return 0;
const elemento = lista.nativeElement;
const alturaVisible = elemento.clientHeight;
const alturaItem = 40; // altura aproximada de cada item
return Math.floor(alturaVisible / alturaItem);
});
constructor() {
// Effect que observa cambios en el tamaño de la lista
effect(() => {
const altura = this.alturaLista();
const lista = this.listaRef();
if (lista && altura > 300) {
// Activar scroll cuando la lista es muy alta
lista.nativeElement.style.maxHeight = '300px';
lista.nativeElement.style.overflowY = 'auto';
}
});
// Effect para logging de cambios
effect(() => {
const cantidad = this.items().length;
const altura = this.alturaLista();
console.log(`Lista actualizada: ${cantidad} items, ${altura}px`);
});
}
agregarItem() {
const id = Date.now();
this.items.update(items => [
...items,
{ id, nombre: `Elemento ${id}` }
]);
}
limpiar() {
this.items.set([]);
}
}
Integración con resource() para datos asíncronos
Las signal queries se combinan eficazmente con la resource API para crear interfaces reactivas que responden tanto a cambios de datos como de DOM:
@Component({
selector: 'app-datos-visuales',
standalone: true,
template: `
<div class="controles">
<input #filtro (input)="aplicarFiltro()" placeholder="Filtrar datos...">
</div>
<div #contenedor class="datos">
@if (datosRecurso.loading()) {
<div class="loading">Cargando datos...</div>
}
@if (datosRecurso.value(); as datos) {
@for (item of datosFiltrados(); track item.id) {
<div class="item-dato">{{ item.nombre }}</div>
}
}
</div>
<div class="estadisticas">
Total: {{ datosFiltrados().length }} elementos
Altura contenedor: {{ alturaContenedor() }}px
</div>
`
})
export class DatosVisualesComponent {
filtroTexto = signal('');
// Resource para datos asíncronos
datosRecurso = resource({
request: () => ({}),
loader: async () => {
// Simular carga de datos
await new Promise(resolve => setTimeout(resolve, 1000));
return [
{ id: 1, nombre: 'Dato A' },
{ id: 2, nombre: 'Dato B' },
{ id: 3, nombre: 'Dato C' }
];
}
});
// Signal queries para elementos del DOM
filtroRef = viewChild<ElementRef<HTMLInputElement>>('filtro');
contenedorRef = viewChild<ElementRef>('contenedor');
// Computed signal para datos filtrados
datosFiltrados = computed(() => {
const datos = this.datosRecurso.value() ?? [];
const filtro = this.filtroTexto().toLowerCase();
if (!filtro) return datos;
return datos.filter(item =>
item.nombre.toLowerCase().includes(filtro)
);
});
// Computed signal para altura del contenedor
alturaContenedor = computed(() => {
const contenedor = this.contenedorRef();
return contenedor ? contenedor.nativeElement.offsetHeight : 0;
});
constructor() {
// Effect que ajusta el scroll cuando cambian los datos filtrados
effect(() => {
const datos = this.datosFiltrados();
const contenedor = this.contenedorRef();
if (contenedor && datos.length > 0) {
// Scroll al inicio cuando cambia el filtro
contenedor.nativeElement.scrollTop = 0;
}
});
}
aplicarFiltro() {
const input = this.filtroRef();
if (input) {
this.filtroTexto.set(input.nativeElement.value);
}
}
}
Composición de queries con linkedSignal
Los linkedSignal pueden utilizarse junto con queries para crear relaciones bidireccionales entre el estado y el DOM:
@Component({
selector: 'app-configurador',
standalone: true,
template: `
<div class="configuracion">
<input #tamanoSlider
type="range"
min="12"
max="24"
[value]="tamaноTexto()"
(input)="actualizarTamano()">
<div #textoMuestra
class="muestra"
[style.font-size.px]="tamaноTexto()">
Texto de ejemplo con tamaño {{ tamaноTexto() }}px
</div>
</div>
`
})
export class ConfiguradorComponent {
// Signal base para el tamaño
tamaноBase = signal(16);
// LinkedSignal que sincroniza con el DOM
tamaноTexto = linkedSignal({
source: this.tamaноBase,
computation: (base) => base,
});
sliderRef = viewChild<ElementRef<HTMLInputElement>>('tamanoSlider');
textoRef = viewChild<ElementRef>('textoMuestra');
constructor() {
// Effect que mantiene sincronizado el slider
effect(() => {
const tamano = this.tamaноTexto();
const slider = this.sliderRef();
if (slider) {
slider.nativeElement.value = tamano.toString();
}
});
// Effect que aplica estilos adicionales basados en el tamaño
effect(() => {
const tamano = this.tamaноTexto();
const texto = this.textoRef();
if (texto) {
const elemento = texto.nativeElement;
// Aplicar clase según el tamaño
elemento.classList.toggle('texto-pequeno', tamano < 16);
elemento.classList.toggle('texto-grande', tamano > 20);
}
});
}
actualizarTamano() {
const slider = this.sliderRef();
if (slider) {
const nuevoTamano = parseInt(slider.nativeElement.value);
this.tamaноTexto.set(nuevoTamano);
}
}
}
La reactividad con signal queries convierte las referencias del DOM en ciudadanos de primera clase del sistema reactivo de Angular, permitiendo crear interfaces que responden fluidamente tanto a cambios de estado como a modificaciones en la estructura del DOM.

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 y funcionamiento de las signal queries viewChild y contentChild.
- Diferenciar entre queries obligatorias y opcionales y su manejo.
- Integrar signal queries con efectos reactivos y computed signals para crear interfaces dinámicas.
- Aplicar signal queries para acceder a contenido proyectado y componentes por tipo.
- Explorar la reactividad avanzada y composición con linkedSignal y resource para datos asíncronos.