Angular 20 ha llegado con nuevas y geniales características y mejoras. Una de las adiciones más significativas es la simplificación de la creación de componentes dinámicos, que la hace mucho más limpia, consistente y predecible. Esta mejora alinea la sintaxis para componentes dinámicos con la forma en que funcionan los “enlaces” (bindings
) en las plantillas de componentes.
Anteriormente, la creación de componentes dinámicos se sentía como el complejo proceso de ensamblar una PC personalizada a partir de piezas dispares y manuales: funcional, sí, pero requería que el desarrollador lidiara manualmente con cada cable, cada conexión y cada compatibilidad. Ahora, Angular 20 te da un panel de control unificado.
El laberinto de la construcción manual (Pre-Angular 20)
Antes de Angular 20, trabajar con componentes creados dinámicamente era engorroso. El “enlace de datos bidireccional” (two-way data binding
) era muy incómodo, y no existía un estilo unificado para trabajar con “entradas” (inputs
) y “salidas” (outputs
).
Para configurar un componente dinámico, era como si tuviéramos que conectar cada componente (input
) y cable de salida (output
) de forma individual y a mano. Para un hipotético NotificationComponent
, los pasos manuales e imperativos eran:
- Conexión manual de componentes: Se tenía que usar
setInput()
para conectar cada componente (propiedad) por separado. - Preparación manual de los datos: Si los datos venían de una “señal” (
signal
), debían ser desenvueltos (acceder a su valor) para obtener el valor real y no la función. - Gestión de las conexiones de salida: Se requería suscribirse manualmente a las salidas (
outputs
), guardando cada suscripción. Era crucial recordar desconectar estos cables (unsubscribe
) para prevenir fugas de memoria. - Sincronización manual de datos: Para que el componente dinámico reaccionara a los cambios en una
signal
del padre, era necesario un “efecto” (effect
) para rastrear el cambio y luego volver a conectar manualmente el cable (setInput
) con el nuevo valor.
El código era como un diagrama de cableado muy imperativo y no reactivo:
// Enfoque anterior (Imperativo)
// Paso 1: Conexión básica de la placa madre
const componentRef = this.container().createComponent(NotificationComponent);
// Paso 2: Conexión manual de los componentes y datos
componentRef.setInput('message', 'Alerta de estado');
componentRef.setInput('priority', this.prioritySignal()); // ¡Desenvolver el valor de la signal!
// Paso 3 & 4: Conexión manual de los cables de salida y de sincronización
this.subscriptions.push(
componentRef.instance.dismissed.subscribe(() => {
componentRef.destroy(); // Lógica de destrucción manual (desconectar del todo)
})
);
// Paso 5: Reactividad manual usando efecto
this.subscriptions.push(
effect(() => {
// Necesario para que el componente se actualice reactivamente
componentRef.setInput('priority', this.prioritySignal());
})
);
// ... Se necesita más código para la sincronización bidireccional y la limpieza de cables.
La forma de acceder a la salida (output
) requería acceder directamente a la propiedad de la clase, lo que no respetaba el “alias” de la salida y era diferente de cómo funciona en las plantillas.
El panel de control unificado (Angular 20)
A partir de Angular 20, la configuración de componentes dinámicos se gestiona a través de la nueva propiedad bindings
. Este nuevo enfoque es declarativo y reactivo, y su corazón es la “referencia del contenedor de vista” (ViewContainerRef
), que actúa como el “socket” principal donde se insertará el nuevo componente.
El poder de ViewContainerRef
Una “referencia del contenedor de vista” (ViewContainerRef
) es el “socket” en el chasis de tu PC donde se puede instalar una tarjeta de video o de sonido. Permite crear, eliminar o incluso mover “tarjetas” (views
) dentro de ese contenedor. Para acceder a él, se puede inyectar con inject()
o, para una ubicación específica, usar una referencia de plantilla.
Para controlar la ubicación exacta de tu componente dinámico, puedes añadir un
en tu plantilla para definir un lugar de inserción:
#container>
Luego, en el código TypeScript, se accede a esa ubicación específica usando viewChild
:
container = viewChild.required("container", { read: ViewContainerRef });
Ahora, la creación del componente dinámico con createComponent
se realiza directamente en ese contenedor, asegurando que se renderice en el lugar deseado.
Configuración con bindings
La propiedad bindings
permite definir “enlaces de entrada” (input
), “enlaces de salida” (output
) y “enlaces bidireccionales” (two-way bindings
) en un estilo unificado. Esto elimina la necesidad de setInput()
manuales y de suscripciones.
Aquí está el código completo para un ejemplo práctico, como un DialogComponent
, que ahora se conecta con un solo “conector modular”:
// Enfoque Angular 20 (Declarativo)
import { Component, inputBinding, outputBinding, viewChild, ViewContainerRef } from '@angular/core';
import { DialogComponent } from './dialog/dialog.component';
@Component({
selector: 'app-root',
template: `
`
})
export class AppComponent {
container = viewChild.required("container", { read: ViewContainerRef });
createDynamic() {
this.container().createComponent(
DialogComponent,
{
bindings: [
// Conexión del cable de entrada 'isOpen'
inputBinding("isOpen", () => true),
// Conexión del cable de entrada 'title'
inputBinding("title", () => "Hello"),
// Conexión del cable de salida 'onClose' para que se autodesconecte
outputBinding("onClose", () => this.container().clear())
]
}
);
}
}
La nueva API gestiona automáticamente la limpieza de suscripciones cuando el componente es destruido, eliminando las pesadillas de gestión de memoria y los cables sueltos.
Las tres funciones de enlace en detalle
La API de “enlaces” (bindings
) se compone de tres funciones esenciales que deben importarse desde @angular/core
.
1. inputBinding()
Esta función se utiliza para conectar las “entradas” (inputs
) del componente:
Argumento | Propósito | Detalles |
---|---|---|
Primer Argumento | El nombre de la entrada. | Es el nombre del conector en la tarjeta. |
Segundo Argumento | El valor. | Es el valor que se transmite. Puede ser un valor estático (un cable con un valor fijo) o una signal (un cable inteligente que transmite actualizaciones). |
Cuando se proporciona una signal
directamente, inputBinding
accede automáticamente al valor de la signal
y rastrea reactivamente los cambios, actualizando la entrada del componente sin necesidad de un effect
manual.
Ejemplos de inputBinding
:
// Entrada estática: el valor no cambia después de la creación
inputBinding('message', () => 'Datos cargados'),
// Entrada dinámica: automáticamente reactiva a los cambios en la signal
inputBinding('priority', this.prioritySignal),
// Entrada calculada: utiliza una función computada (también reactiva)
inputBinding('statusText', () => `Usuario: ${this.currentUser()} - Estado: ${this.systemStatus()}`),
2. outputBinding()
Esta función maneja los “eventos de salida” (outputs
) emitidos por el componente dinámico.
Argumento | Propósito | Detalles |
---|---|---|
Primer Argumento | El nombre de la salida. | Es el nombre del cable de salida que emite un evento. |
Segundo Argumento | Una función de “devolución de llamada” (callback ). |
Es la lógica que se ejecuta cuando el cable emite un evento. |
Se puede especificar el tipo de valor emitido usando un “tipo genérico” (generic type
) para mejorar la seguridad de tipos en TypeScript:
// Salida básica: Maneja el evento de cierre
outputBinding('dismissed', () => {
console.log('Notificación cerrada!');
}),
// Salida con datos y tipado (UserEvent es un tipo genérico)
outputBinding<UserEvent>('userAction', (eventData) => {
this.handleAction(eventData);
}),
3. twoWayBinding()
Esta es la solución simplificada para el “enlace de datos bidireccional” (two-way data binding
), reemplazando el cableado manual complejo.
Simplemente se define el nombre de la “entrada modelo” (input model
) y se proporciona la signal
del componente padre que se desea mantener sincronizada.
// Sincroniza la propiedad 'isUrgent' del componente con 'this.urgentModeSignal' del padre
twoWayBinding('isUrgent', this.urgentModeSignal),
Angular maneja automáticamente la convención de sufijo Change
(por ejemplo, isUrgent
se empareja con la salida isUrgentChange
).
Directivas aplicadas en tiempo de ejecución
Angular 20 también permite añadir componentes o características adicionales (directives
) al componente creado dinámicamente. Esto se logra mediante la propiedad directives
en la configuración de createComponent
.
Para directivas sencillas, se proporciona el nombre del constructor de la directiva. Para directivas más complejas que requieren configuración, se proporciona un objeto que incluye la clave type
(el constructor de la directiva) y la propiedad bindings
para configurar sus propias entradas.
Ejemplo de directivas de runtime:
Configuraremos un NotificationComponent
con una directiva simple (FocusHighlightDirective
) y una directiva compleja de la librería Material (SnackbarDirective
) para mostrar mensajes de estado al pasar el ratón:
const componentRef = this.container().createComponent(NotificationComponent, {
bindings: [
inputBinding('message', () => 'Mueva el ratón para ver más detalles')
],
directives: [
// Directiva simple sin configuración
FocusHighlightDirective,
// Directiva compleja con configuración de entradas
{
type: SnackbarDirective, // La clase de la directiva
bindings: [
// Configura las entradas de la directiva
inputBinding('snackbarMessage', () => 'Información extra al hacer hover'),
inputBinding('snackbarPosition', () => 'right')
]
}
]
});
Esta capacidad permite un diseño más modular y una configuración dinámica de la experiencia del usuario.
En resumen, el nuevo enfoque de Angular 20 hace que la creación de componentes dinámicos sea más consistente, limpia y legible, ya que la sintaxis de “enlace” (binding
) ahora es idéntica a la de las plantillas de Angular, además de ofrecer reactividad incorporada y gestión simplificada de la memoria. Ahora, en lugar de ensamblar una PC a mano, utilizas un “panel de control” unificado para la configuración y conexión de tus componentes.