Apariencia
🔍 Clase 6 — ViewChild y ContentChild
📅 2026-07-18 · 🗂️ Proyecto:
AppClase04(destino en este repo:app-semana04) · 🎯@ViewChild()y@ContentChild()
🗂️ Temas de esta clase
viewChild(): acceder desde la clase TS a un elemento del propio template del componente, usando la API de signals (no el decorador clásico@ViewChild()).@ContentChild(): acceder a un elemento o componente que llega por<ng-content>(contenido proyectado desde el padre) — pendiente de ver.
🗺️ Diagrama: la diferencia entre los dos

viewChild()lee algo que el propio componente ya tiene en su HTML — el<input #campoNombre>vive dentro deformulario.html, junto con la clase que lo lee.contentChild()lee algo que no está en el HTML del componente, sino que el padre lo escribe entre las etiquetas del hijo (<app-caja>...</app-caja>) y Angular lo proyecta adentro vía<ng-content>.
🧩 viewChild(): referenciar un <input> del propio template
Componente formulario, con un botón que enfoca el <input> por código:
ts
import { Component, viewChild } from '@angular/core';
@Component({
selector: 'app-formulario',
imports: [],
templateUrl: './formulario.html',
styleUrl: './formulario.css',
})
export class Formulario {
campoNombre = viewChild<HTMLInputElement>('campoNombre');
enfocar() {
this.campoNombre()?.focus();
}
}html
<input #campoNombre type="text" placeholder="Tu nombre" />
<button (click)="enfocar()">Enfocar Input</button>#campoNombre(en el HTML) es una template reference variable: un "gancho" con nombre sobre ese<input>concreto, visible solo dentro de ese mismo template.viewChild<HTMLInputElement>('campoNombre')— la función (nueva API de signals, Angular 17.3+) busca en el template el elemento marcado con#campoNombrey devuelve un signal con la referencia. El genérico<HTMLInputElement>le dice a TypeScript qué tipo de elemento es, para poder usar.focus()sin error de tipos.campoNombre()— como es un signal, para leer el valor se llama como función (con paréntesis).?.focus()porque al principio (antes de que Angular pinte la vista) el signal puede valerundefined.(click)="enfocar()"— Event Binding de repaso: al hacer clic, se llama al método que usa la referencia para enfocar el<input>por código (sin que el usuario haga clic directo en la cajita de texto).
📌
viewChild()(función/signal) vs@ViewChild()(decorador clásico): hacen lo mismo (referenciar algo del propio template), peroviewChild()es la forma moderna (Angular 17.3+), devuelve un signal de solo lectura y no necesita el hookngAfterViewInitpara leer el valor con seguridad (antes, con el decorador clásico, había que esperar angAfterViewInitporque la referencia llegabaundefinedhasta ese momento). → verciclo-de-vida.mdpara ese hook.
🧪 Tip de entrevista: ¿Por qué
campoNombre()?.focus()y nocampoNombre.focus()directo? →campoNombrees un signal (una función que envuelve el valor), no el elemento en sí. Hay que llamarlo (campoNombre()) para obtener elHTMLInputElementreal; el?.es porque puede no existir todavía (antes de que la vista se termine de renderizar).
⚠️ Error real que salió armando esto: el profe escribió
this.campoNombre()?.nativeElement.focus(), mezclando la sintaxis del decorador clásico@ViewChild(que envuelve el resultado enElementRef, y por eso hace falta.nativeElementpara llegar al elemento real) con la función nuevaviewChild<HTMLInputElement>(...)(que, al pedirle el tipoHTMLInputElementdirectamente, ya devuelve el elemento, sin envoltorio). El compilador lo marca conTS2339: Property 'nativeElement' does not exist on type 'HTMLInputElement'. La versión correcta es sin.nativeElement:this.campoNombre()?.focus().
📦 contentChild(): leer texto de contenido proyectado (<ng-content>)
Componente caja — a diferencia de formulario (que referencia algo de su propio template), este lee un elemento que le llega de afuera, proyectado por el padre:
ts
import { Component, contentChild, ElementRef, AfterContentInit } from '@angular/core';
@Component({
selector: 'app-caja',
imports: [],
templateUrl: './caja.html',
styleUrl: './caja.css',
})
export class Caja {
titulo = contentChild<ElementRef>('titulo');
ngAfterContentInit(): void {
console.log('Texto recibido: ', this.titulo()?.nativeElement.textContent);
}
}contentChild<ElementRef>('titulo')— la versión decontentChildpara contenido proyectado (lo que el padre pone dentro de<app-caja>...</app-caja>, vía<ng-content>en el template deCaja). Busca el elemento marcado#tituloentre el contenido proyectado, no en el propio template deCaja.<ElementRef>en vez de<HTMLInputElement>— aquí el genérico sí esElementRefa propósito, por eso.nativeElement.textContentes correcto (al revés del error que salió enviewChild<HTMLInputElement>()deformulario, donde.nativeElementsobraba). Comparar los dos ayuda a fijar la regla: el tipo que le pides al genérico es exactamente lo quetitulo()/campoNombre()te va a devolver.ngAfterContentInit()— hook del ciclo de vida que se dispara después de que Angular termina de proyectar el contenido (<ng-content>) por primera vez. Por eso se leecontentChildahí, y no enngOnInit(que corre antes, cuando el contenido proyectado todavía no está listo). → verciclo-de-vida.md.
📄 caja.html y cómo se usa desde app.html
html
<!-- caja.html -->
<div style="border:2px solid green;">
<ng-content></ng-content>
</div>html
<!-- app.html -->
<app-caja>
<h2 #titulo> Este texto viene de afuera </h2>
</app-caja><ng-content></ng-content>— es el "hueco" donde Angular inserta lo que el padre ponga entre<app-caja>y</app-caja>. El<div>con borde verde es solo un marco visual para ver dónde termina el componenteCajay dónde empieza el contenido proyectado.<h2 #titulo>— esto lo escribe el padre (App), noCaja. Es el contenido que se proyecta; como tiene la referencia#titulo,contentChild<ElementRef>('titulo')enCajalo encuentra yngAfterContentInitimprime su texto:"Texto recibido: Este texto viene de afuera".- Diferencia clave con
viewChild(formulario): ahí#campoNombreestaba dentro del propio template deFormulario; aquí#tituloestá fuera, en el template del padre, y solo "aparece" dentro deCajagracias a<ng-content>.
▶️ Cómo correrlo
$ cd 02-Ejercicios/app-semana04
$ npm install // solo la primera vez
$ npm start // = ng serve → http://localhost:4200/En http://localhost:4200/ ya están cableados en app.html: <app-formulario> (el <input> con el botón "Enfocar Input" que usa viewChild()) y <app-caja> (con el <h2 #titulo> proyectado adentro, dentro del recuadro verde, que contentChild() lee por consola en ngAfterContentInit).
🏋️ Ejercicios con solución
Ejercicio 1 — viewChild() sobre un segundo elemento del propio template
En formulario.html agrega un segundo <input> (por ejemplo, uno de email) con su propia referencia, y un botón que lo enfoque a él en vez del primero. Usa la misma función viewChild() que ya viste con campoNombre.
Ver solución
html
<!-- formulario.html -->
<input #campoNombre type="text" placeholder="Tu nombre" />
<button (click)="enfocar()">Enfocar Input</button>
<input #campoEmail type="email" placeholder="Tu email" />
<button (click)="enfocarEmail()">Enfocar Email</button>ts
// formulario.ts
import { Component, viewChild } from '@angular/core';
@Component({
selector: 'app-formulario',
imports: [],
templateUrl: './formulario.html',
styleUrl: './formulario.css',
})
export class Formulario {
campoNombre = viewChild<HTMLInputElement>('campoNombre');
campoEmail = viewChild<HTMLInputElement>('campoEmail');
enfocar(): void {
this.campoNombre()?.focus();
}
enfocarEmail(): void {
this.campoEmail()?.focus();
}
}Misma regla que con campoNombre: #campoEmail es la template reference variable en el HTML, viewChild<HTMLInputElement>('campoEmail') la busca en el propio template de Formulario, y campoEmail()?.focus() se llama como signal (con paréntesis) porque puede valer undefined antes de que la vista termine de renderizarse.
Ejercicio 2 — contentChild() con otro elemento proyectado
En vez de proyectar un <h2 #titulo> dentro de <app-caja>, prueba proyectar un <p #descripcion> y léelo desde Caja con contentChild(), imprimiendo su texto en ngAfterContentInit.
Ver solución
html
<!-- app.html -->
<app-caja>
<p #descripcion> Esta descripción también viene de afuera </p>
</app-caja>ts
// caja.ts
import { Component, contentChild, ElementRef, AfterContentInit } from '@angular/core';
@Component({
selector: 'app-caja',
imports: [],
templateUrl: './caja.html',
styleUrl: './caja.css',
})
export class Caja implements AfterContentInit {
descripcion = contentChild<ElementRef>('descripcion');
ngAfterContentInit(): void {
console.log('Descripción recibida: ', this.descripcion()?.nativeElement.textContent);
}
}caja.html no cambia (sigue con el mismo <ng-content></ng-content> genérico): no le importa qué proyecta el padre, solo que quien lo proyecte tenga la referencia (#descripcion) que contentChild() está buscando. Como en el ejemplo original con #titulo, el genérico es <ElementRef> (no <HTMLParagraphElement>), así que .nativeElement.textContent es correcto.
❓ Preguntas y respuestas
1. ¿Cuál es la diferencia clave entre viewChild() y contentChild()?
viewChild()lee un elemento que está dentro del propio template del componente (ejemplo:#campoNombreenformulario.html, junto con la clase que lo lee).contentChild()lee un elemento que el padre proyecta desde afuera, vía<ng-content>(ejemplo:#tituloqueAppescribe entre<app-caja>...</app-caja>).
2. ¿Por qué contentChild se lee en ngAfterContentInit y no en ngOnInit?
Porque
ngOnInitse ejecuta antes de que Angular termine de proyectar el contenido del padre — en ese momentotitulo()todavía valdríaundefined.ngAfterContentInitse dispara después de la primera proyección de<ng-content>, que es cuando la referencia ya está disponible con seguridad. → verciclo-de-vida.md.
3. En campoNombre = viewChild<HTMLInputElement>('campoNombre') vs titulo = contentChild<ElementRef>('titulo'), ¿por qué el genérico es distinto?
El genérico define exactamente lo que la función va a devolver al llamarla. Pedir
<HTMLInputElement>hace quecampoNombre()entregue el elemento ya "pelado", por eso.focus()va directo, sin.nativeElement. Pedir<ElementRef>hace quetitulo()entregue el envoltorioElementRef, por eso hace falta.nativeElement.textContentpara llegar al elemento real. Mezclar ambos (pedirHTMLInputElementy después escribir.nativeElement) es justo el error que salió documentado más arriba.
4. ¿viewChild() y contentChild() devuelven el valor directo o hay que llamarlos?
Son signals: hay que llamarlos como función (
campoNombre(),titulo()) para leer el valor actual. Por eso siempre aparecen con?.antes del resto (?.focus(),?.nativeElement) — al principio, antes de que Angular termine de pintar la vista (o de proyectar el contenido), el signal puede valerundefined.
❓ Pendiente
- [x] Código real de
contentChild()(ya confirmado, conngAfterContentInit). - [x]
caja.htmlreal (con<ng-content>) y cómo lo usaapp.html(el contenido proyectado con#titulo). - [x] Diferencia práctica entre
viewChildycontentChild:viewChildlee tu propio template,contentChildlee lo que el padre te proyecta vía<ng-content>. - [ ] Ver si el profe también muestra el decorador clásico
@ViewChild()/@ContentChild()para comparar con las funciones nuevas. - [ ] Confirmar si
Cajatermina conimplements AfterContentInit(el import ya está).
📎 Apuntes relacionados
ciclo-de-vida.md— los hooks (ngAfterViewInit,ngAfterContentInit) suelen aparecer junto conviewChild/contentChild, porque la referencia recién está disponible después de esos momentos del ciclo de vida.02-Conceptos.md— se resumirá aquí también en cuanto haya código confirmado.
➡️ Siguiente
Ver si aparece el decorador clásico @ViewChild()/@ContentChild() para comparar con las funciones de signals ya vistas, y confirmar implements AfterContentInit en Caja.