Angular 20: De la programación imperativa a la creación declarativa de componentes dinámicos


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:

  1. Conexión manual de componentes: Se tenía que usar setInput() para conectar cada componente (propiedad) por separado.
  2. 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.
  3. 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.
  4. 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.
Enter fullscreen mode

Exit fullscreen mode

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>
Enter fullscreen mode

Exit fullscreen mode

Luego, en el código TypeScript, se accede a esa ubicación específica usando viewChild:

container = viewChild.required("container", { read: ViewContainerRef });
Enter fullscreen mode

Exit fullscreen mode

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())
        ]
      }
    );
  }
}
Enter fullscreen mode

Exit fullscreen mode

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()}`),
Enter fullscreen mode

Exit fullscreen mode



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);
}),
Enter fullscreen mode

Exit fullscreen mode



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),
Enter fullscreen mode

Exit fullscreen mode

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')
      ]
    }
  ]
});
Enter fullscreen mode

Exit fullscreen mode

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.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *