Hay un momento en la evolución de cualquier desarrollador Angular donde deja de construir componentes “bonitos” y empieza a construir sistemas realmente escalables. Porque la realidad es que:
Angular no funciona únicamente renderizando HTML. Angular funciona creando, destruyendo e insertando vistas dinámicamente todo el tiempo.
Cada vez que escribes algo como:
@if(visible) { }
@for(item of items; track $index) { }
Angular internamente termina trabajando con:
- Templates
- Referencias de vistas
- Contenedores dinámicos
- Inserción de vistas y destrucción de vistas
Y aquí es donde aparecen herramientas importantes del framework, precisamente:
ng-templateng-containerng-contentTemplateRefngTemplateOutletViewContainerRef
La mayoría de desarrolladores conoce sus nombres, pero honestamente ¿los entendemos realmente? intentemos conocerlos un poco más
⚠️ El problema de muchos componentes Angular
Comienzan simples pero con el tiempo terminan así:
<table-component
[showIcons]="true"
[showActions]="true"
[compact]="false"
[customHeader]="true"
[editable]="true"
[grouped]="true"
/>
Y por dentro:
- Condiciones enormes con múltiples
@if , @else if… - Configuraciones infinitas
- Lógica acoplada al HTML
- Componentes difíciles de extender/mantener ( y entender jeje )
Seamos sinceros, Angular normalmente no es el problema 👀
El problema es que seguimos construyendo componentes rígidos para problemas dinámicos.
🧩 Angular ya resolvió este problema hace años
Y lo hizo mediante vistas dinámicas, con ese motor de composición visual. Estas son las piezas principales:
| Concepto | Qué hace |
|---|---|
ng-template | Define contenido reutilizable no renderizado |
TemplateRef | Referencia en TypeScript a un template |
ngTemplateOutlet | Inserta un template en el DOM |
ViewContainerRef | Crea y destruye vistas dinámicamente |
ng-container | Contenedor lógico sin HTML real |
ng-content | Proyección de contenido desde el padre |
La parte interesante es que todas trabajan juntas no son herramientas separadas son piezas del mismo sistema.
ng-template: HTML que todavía no existe
<ng-template #cardTemplate>
<div class="card">Angular 21</div>
</ng-template>
Muchos creen que Angular ya renderizó ese HTML pero realmente no existe visualmente. No ocupa espacio. No aparece en el DOM como un elemento visible. Angular únicamente guarda una definición interna de cómo construir esa vista cuando sea necesario. Veamoslo como:
- Una estrategia de renderizado
- Un layout reutilizable
- Una vista diferida
- Una pieza dinámica de UI
En otras palabras, Angular no almacena el resultado final, almacena las instrucciones necesarias para construirlo cuando alguien lo solicite.
Ojo Angular no renderiza templates hasta que alguien se lo pide…
¿Quién puede pedirle a Angular que renderice un ng-template?
Principalmente tres mecanismos:
- ngTemplateOutlet
- ViewContainerRef
- Las directivas estructurales del propio framework (@if, @for, @switch, etc.)
TemplateRef: el puente entre HTML y TypeScript
Cada vez que Angular encuentra:
<ng-template #cardTemplate />
Internamente crea un TemplateRef eso significa que el template ya no es solamente HTML. Ahora también existe como una referencia manipulable desde TypeScript. A la cual podemos acceder:
viewChild<TemplateRef<any>>("cardTemplate");
A partir de aquí puedes:
- Renderizar el template manualmente
- Reutilizarlo
- Moverlo y/o destruirlo
- Crear múltiples instancias
- Enviar contexto dinámico
TemplateRef todavía no renderiza nada. Representa las instrucciones que Angular utilizará para construir una vista.
ng-container: un contenedor que no existe
Angular necesitaba una forma de insertar lógica estructural sin contaminar el DOM y por eso existe <ng-container />, no genera HTML ni aparece visualmente. Es simplemente un contenedor lógico donde Angular puede operar.
Por eso casi siempre verás:
<ng-container *ngTemplateOutlet="template" />
Es una referencia a un TemplateRef, que a su vez proviene de un ng-template.
ngTemplateOutlet: el momento donde Angular materializa la vista
<ng-container [ngTemplateOutlet]="template" />
Lo que Angular entiende aquí es:
“Toma este
TemplateRefy conviértelo en una vista real dentro del DOM”.
Es decir:
ng-template → TemplateRef → ngTemplateOutlet → Vista renderizada
A partir de aquí, el template deja de estar acoplado a su ubicación original, se convierte en una pieza reutilizable que puedes definir en un sitio, renderizar en otro, repetir múltiples veces o sustituir dinámicamente según el estado de la aplicación.
El siguiente salto aparece cuando ese template empieza a recibir datos. Cada <ng-template> puede definir un contexto de ejecución usando let-, que no es más que un mapeo entre el contexto que envías y las variables locales del template. Si no defines una clave explícita, Angular asigna automáticamente el valor principal a $implicit.
<ng-template #userTemplate let-user>
<h3>{{ user.name }}</h3>
</ng-template>
<ng-container
[ngTemplateOutlet]="userTemplate"
[ngTemplateOutletContext]="{
$implicit: currentUser
}"
/>
En este caso, currentUser se inyecta directamente como user dentro del template sin necesidad de mapeo adicional.
Cuando necesitas más control, el contexto pasa a ser un objeto estructurado y puedes mapear múltiples valores explícitamente usando let-x="key":
<ng-template let-name="name" let-price="price">
<div>{{ name }} - {{ price }}</div>
</ng-template>
<ng-container
[ngTemplateOutlet]="productTemplate"
[ngTemplateOutletContext]="{
name: 'MacBook Pro',
price: 2499
}"
/>
Aquí cada propiedad del contexto se enlaza directamente con una variable del template, manteniendo claridad y desacoplamiento total.
Angular también expone variables reservadas cuando trabajas con estructuras iterativas o vistas dinámicas como $implicit, index, first, last, even y odd, lo que permite construir templates completamente genéricos sin acoplarlos a la forma concreta de los datos.
Al final, un TemplateRef no es más que la referencia programable de un <ng-template> desde TypeScript. Y ngTemplateOutlet es el mecanismo que convierte esa definición en una vista real.
Renderizado controlado desde Angular, pero también existe la forma de que el contenido no lo defina Angular sino el consumidor del componente 😉 ng-content
ng-content: composición real de componentes
Sirve para mostrar contenido que viene desde fuera del componente porque el componente deja de controlar completamente su contenido interno. Ahora el padre puede decidir qué renderizar.
card.component.html
<div class="card">
<ng-content />
</div>
Uso:
<card-component>
<h2>Título dinámico</h2>
<p>Contenido dinámico</p>
</card-component>
Aquí Angular no instancia nada dinámicamente: simplemente proyecta lo que ya existe desde fuera.
“Toma nodos ya existentes del árbol del componente padre y los proyecta dentro del componente hijo”.
ViewContainerRef: dinámico desde TypeScript
Aquí ya no renderizas templates de forma declarativa. Ahora puedes crear vistas dinámicamente desde TypeScript.
Si ngTemplateOutlet es la forma declarativa de decir “renderiza esto aquí”, ViewContainerRef es la versión imperativa: “crea, inserta o destruye esta vista cuando yo lo decida desde TypeScript”.
this.viewContainer.createEmbeddedView(this.template, {
$implicit: data,
});
Ojo: ng-content = composición declarativa del árbol (nivel plantilla / markup) - ViewContainerRef = composición dinámica del árbol de vistas (nivel runtime)
Lo importante aquí es cuando empiezas a trabajar así, el componente deja de ser el único responsable del renderizado. Puedes mover esa responsabilidad a capas superiores (servicios o helpers), lo que te permite construir sistemas más flexibles como:
- renderizadores de tablas dinámicas
- motores de formularios basados en metadata
- layouts configurables en runtime
- sistemas de plugins UI
O incluso pensar en un motor de renderizado dinámico, donde Angular deja de ser un framework de componentes y pasa a ser una plataforma de composición de vistas en tiempo de ejecución.
Entonces… ¿por qué construyo componentes rígidos? 🤔
Cuando entiendes realmente estas herramientas, cambia tu forma de construir y pinsas más en
- Composición
- Renderizado dinámico
- Estrategias visuales
- Templates reutilizables
- APIs visuales configurables
Quiero que antes de crear un componente te hagas estas preguntas
- ¿El contenido realmente debe ser fijo?
- ¿Estoy acoplando la lógica de negocio con la capa de presentación?
- ¿Necesito renderizar variaciones del mismo layout?
- ¿Estoy duplicando estructuras?
Aun te quedaron ganas ? te dejo un ejemplo completo
1.- Componente para agrupar directivas que daran datos y acciones en posiciones concretas a un componente por template
@Component({
selector: 'table-column',
template: ''
})
export class TableColumnComponent {
name = input.required<string>();
title = input<string>();
width = input<number>();
headerTemplate = contentChild<TableHeaderTmplDirective>(TableHeaderTmplDirective);
cellTemplate = contentChild<TableCellTmplDirective>(TableCellTmplDirective);
}
2.- Ejemplo de las directivas como TemplateRef
@Directive({
selector: '[TableHeaderTmpl]'
})
export class TableHeaderTmplDirective {
templateRef = inject(TemplateRef);
}
2.- Componente que manejará la table, solo mostraremos como se usarán los template
@Component({
selector: 'table-component',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
imports: [NgTemplateOutlet]
})
export class TableComponent {
data = input<any[]>()
private readonly columns = contentChildren<TableColumnComponent>(TableColumnComponent);
}
3.- HTML de table.component que controla los templates
<table class="table">
<thead>
<tr>
@for (column of columns(); track column.name()) {
<th>
@if (column.headerTemplate()) {
<ng-container *ngTemplateOutlet="column.headerTemplate()!.templateRef"/>
} @else {
{{ column.title() }}
}
</th>
}
</tr>
</thead>
<tbody>
@for (item of data(); track item.id) {
<tr>
@for (column of columns(); track column.name()) {
<td>
@if (column.cellTemplate()) {
<ng-container *ngTemplateOutlet=" column.cellTemplate()!.templateRef;
context: {
$implicit: item,
value: item[column.name()]
}
"
/>
} @else {
{{ item[column.name()] }}
}
</td>
}
</tr>
}
</tbody>
</table>
<div>
<ng-content select="[table-legend]" />
</div>
4.- Uso del componente
Ahora el consumidor puede inyectar contenido arbitrario en zonas concretas del componente.
<table-component [data]="users">
<table-column name="name" title="Usuario">
<ng-template TableCellTmpl let-user>
<div class="user-cell">
<strong>{{ user.name }}</strong>
</div>
</ng-template>
</table-column>
<table-column name="role" title="Rol">
<ng-template TableCellTmpl let-user>
<span>
{{ user.role }}
</span>
</ng-template>
</table-column>
<table-column name="actions">
<ng-template TableHeaderTmpl>
Acciones (podrias usar otra directiva para representar acciones diferentes)
</ng-template>
<ng-template TableCellTmpl let-user>
<button>
Editar {{ user.name }}
</button>
</ng-template>
</table-column>
<div table-legend>
Total usuarios: {{ users.length }}
</div>
</table-component> Written by: Dayerlin Bustamante