Composición de directivas en Angular

Alejandro Cuba Ruiz
ngconf
Published in
5 min readAug 4, 2023

--

En 2016, cuando la versión candidata de Angular 2 estuvo disponible después de una larga serie de lanzamientos alpha y beta, James Friedman iniciaba un hilo de discusión que eventualmente condujo al anuncio de la API de composición de directivas en Angular 15.

Con el lanzamiento de esta versión hace ya casi un año, el equipo de Angular incorporó una pieza que hoy en día es considerada una de las adiciones recientes más útiles a este framework.

Un vistazo a sus beneficios

El concepto de composición de directivas es útil porque permite juntar el comportamiento de directivas separadas en un solo elemento a través de una sencilla declaración.

La propiedad hostDirectives actualmente disponible en el decorador de las clases de directivas y componentes facilita la reutilización de directivas, mejorando así la experiencia del desarrollador.

Vamos a comenzar con un ejemplo donde declaremos un componente de Angular que permita redimensionar y reordenar widgets en una interfaz web:

El componente del dashboard renderiza un widget con dos atributos de directivas.

En el código se representa un componente que importa un WidgetComponent y dos directivas que afectan su comportamiento. En la plantilla HTML, se instancia el elemento web app-widget con los atributos de directivas que permiten mover y redimensionar al widget dentro del dashboard.

Vamos a referirnos al código correspondiente a las directivas solo con fines ilustrativos. En proyectos reales que requieren interacciones de usuario complejas, es recomendable obtener dichas funcionalidades directo del Angular CDK u otra biblioteca de interfaz de usuario, para no tener que destinar recursos propios a programar algo ya implementado y extensivamente revisado por terceros.

La directiva que provee el redimensionamiento.
La directiva que provee la facilidad de arrastrar y soltar el elemento.

La idea es tener a laResizableWidgetDirective encapsulando la lógica que controla el tamaño del widget basado en eventos específicos del ratón, y a la DraggableWidgetDirective añadiendo la funcionalidad que los usuarios esperan para poder moverlos a través del dashboard.

En este punto, podemos movernos desde la forma tradicional de declarar directivas hacia el nuevo método de composición. Al incluir la propiedad hostDirectives en el código del componente widget, ya no es necesario referenciar atributos de directiva en la plantilla del componente padre.

A estas referencias vamos a denominarlas "directivas anfitrionas", como intento de traducción del término original en inglés host directive.

El component widget con las directivas anfitrionas referenciadas.
El widget ya no necesita los atributos de directiva especificados desde el componente padre.

Así hemos eliminado la necesidad de declarar las directivas del widget en el componente del dashboard. El WidgetComponent ahora encapsula cada aspecto relacionado con el widget propiamente.

Dependiendo de la arquitectura que se desee establecer para implementar el dashboard, podríamos elegir componer las directivas nativas NgIf y NgFor dentro del WidgetComponent. Esto permitiría renderizar programáticamente la lista de widgets desde el código TypeScript del componente.

Además, podríamos utilizar la técnica de composición de directivas para extender la funcionalidad de bibliotecas de interfaz de usuario externas. Por ejemplo, la directiva cdkDrag del Angular CDK podría declararse como anfitriona de la DraggableWidgetDirective para reutilizar y expandir su comportamiento predeterminado.

Incluso si la mezcla de directivas nativas con las nuestras no es un requisito técnico, el simple hecho de componer directivas relacionadas puede mejorar significativamente el mantenimiento de la aplicación. Este enfoque permite lograr que nuestro código sea más modular y fácil de revisar.

Expandiendo la declaración de directivas anfitrionas

En algún momento puede que necesitemos emitir valores desde — o pasar datos hacia — alguna directiva anfitriona. En lugar de proporcionar a hostDirectives un simple listado de referencias a directivas autónomas (o standalone), podríamos hacer uso de la declaración expandida de directivas anfitrionas que contiene las propiedades input y output.

Para ilustrar esto, extendamos el ejemplo anterior, incorporando a la DraggableWidgetDirective dos propiedades decoradas con declaraciones de entrada y salida:

La directiva de reposicionamiento incluyendo propiedades de entrada y salida.
El widget referenciando las propiedades de entrada y salida de dicha directiva.
El componente del dashboard interactuando con la entrada y salida de la directiva anfitriona.

Como vemos en este ejemplo simplificado, la plantilla del componente del dashboard interactúa con las propiedades de entrada y salida de la directiva anfitriona DraggableWidgetDirective a través de la vinculación de atributos y eventos.

Ya que tenemos configurado este flujo básico de datos entre componentes padre e hijo, pasemos a la fase de compilación del código.

Compilando directivas anfitrionas

Es durante el proceso de compilación, no en tiempo de ejecución, que las directivas anfitrionas se aplican estáticamente a los componentes o directivas que los declararon. Aquí el compilador verifica la configuración de cada directiva anfitriona, que debe cumplir con los siguientes requisitos:

  • Las directivas anfitrionas solo deben hacer referencia a clases anotadas con un decorador de directiva.
  • Las directivas anfitrionas referenciadas deben coincidir con un elemento solamente una vez, para evitar duplicados.
  • Las directivas anfitrionas referenciadas deben ser directivas autónomas que no requieren ser declaradas desde un módulo de Angular.
  • Las directivas anfitrionas referenciadas no pueden ser componentes, ya que los componentes en Angular son un tipo especial de directivas con lógica de plantilla añadida.
export interface HostDirectiveMeta {
directive: Reference<ClassDeclaration>;
isForwardReference: boolean;
inputs: {[publicName: string]: string}|null;
outputs: {[publicName: string]: string}|null;
}

La interfaz anterior, HostDirectiveMeta, definida en el código fuente de los metadatos del compilador, describe las propiedades de una directiva anfitriona. Ahí se puede leer la referencia a la clase de la directiva anfitriona, más las entradas y salidas expuestas.

Esta interfaz de metadatos también contiene la propiedad booleana isForwardReference para indicar si la directiva anfitriona es una referencia adelantada (forward reference). Esto puede ayudar al compilador de Angular con casos poco comunes de dependencia circular al importar directivas autónomas. Sin embargo, no podemos acceder a esta propiedad directamente a través de hostDirectives. Su función principal es durante el proceso de compilación, cuando la información de la directiva anfitriona se transfiere a los metadatos que le corresponden.

La composición excesiva como un antipatrón

En una situación hipotética con el ejemplo del dashboard, podría resultar tentador continuar agregando directivas anfitrionas al WidgetComponent para mejorar las capacidades de los widgets.

Supongamos que el supervisor de la aplicación web solicita una serie de nuevas funcionalidades para mejorar el dashboard: tematización basada en widgets, personalización del widget basado en la ubicación del usuario y la posibilidad de encender o apagar elementos específicos dentro del widget a través de algún sistema de gestión de funcionalidades (Feature Management System). Además, el dashboard en dispositivos móviles debe lograr que los widgets se inclinen en 3D según la posición del dispositivo.

Imaginemos entonces que, por razones subóptimas, de pronto nos encontramos convencidos de que es viable cumplir con todos estos requisitos a través de la composición de tantas directivas.

El widget con tanta referencia a directivas anfitrionas que el rendimiento web puede ser afectado.

Incluso antes de escribir la primera línea de código es evidente que hay algo mal con esta aproximación. Poniendo aparte la discusión sobre el enfoque dudoso para implementar los nuevos requerimientos, el uso excesivo de directivas anfitrionas puede impactar significativamente los recursos del navegador web. Con cada widget, Angular no solo crea un objeto de la clase WidgetComponent sino también una instancia de cada directiva anfitriona, necesitando mucho más espacio de memoria.

Al igual que casi cualquier otro aspecto del desarrollo web, la composición de directivas en Angular requiere una planificación y monitoreo cuidadosos para evaluar su impacto en el navegador. Al medir la complejidad resultante con Angular DevTools y el rendimiento con Lighthouse — o cualquier otra herramienta de diagnóstico — podemos obtener información valiosa sobre las consecuencias de agrupar múltiples directivas anfitrionas.

Conclusión

La nueva API de composición de directivas simboliza la evolución de Angular, proporcionando un sólido mecanismo para reutilizar funcionalidades en diferentes componentes y directivas. Tengamos siempre presente que el uso excesivo de directivas anfitrionas podría afectar negativamente el rendimiento de las aplicaciones web.

--

--

ngconf
ngconf

Published in ngconf

The World’s Best Angular Conference

Alejandro Cuba Ruiz
Alejandro Cuba Ruiz

Written by Alejandro Cuba Ruiz

<front-end web engineer />, Angular GDE, traveler, reader, writer, human being.