Angular Directive Composition
In 2016, when release candidates for Angular 2 became available following a lengthy series of alpha and beta versions, James Friedman started a thread that eventually led to the announcement of the directive composition API in version 15.0.0.
With the release of this version almost a year ago, the Angular core team introduced a piece now regarded as one of the most valuable recent additions to the framework.
Understanding its benefits
The directive composition concept is helpful because it enables us to put together the behavior of separate directives into one single host element with minimal effort.
The hostDirectives
property available in directive and component class decorators aims to promote reusability practices and improve the developer experience.
Let's consider a component declaration that allows resizing and reorganizing widgets on a flexible dashboard:
This code example represents a standalone root component, importing a WidgetComponent
and two directives that will affect its behavior. In the HTML template, an app-widget
element is instantiated, and the custom attribute directives are applied to it, making the widget resizable and draggable.
Moving on to the directives, the provided code was written just for illustrative purposes. For real-world projects dealing with complex user interactions, consider sourcing such functionalities from the Angular CDK or other actively maintained third-party UI libraries.
The idea is to have a ResizableWidgetDirective
encapsulating the logic that controls the widget size based on specific mouse events, while the DraggableWidgetDirective
adds the corresponding behavior users expect to move them around.
Now, it's time to use directive composition by including the hostDirectives
property in the widget component code. This eliminates the need to reference attribute directives in the parent component template.
We have eliminated the need for directive declarations in the parent component. The WidgetComponent
now encapsulates every aspect related to the widget itself.
Depending on the architecture you wish to set up for the dashboard, you could choose to compose the built-in NgIf
and NgFor
directives at the WidgetComponent
level. This would allow you to programmatically render the list of widgets from the component TypeScript code.
Also, we could use the directive composition technique to extend the functionality of external UI libraries. For example, the Angular CDK's cdkDrag
directive could be declared a DraggableWidgetDirective
's host directive to reuse and expand the default behavior.
Even if mixing built-in and custom directives isn’t a technical requirement, simply composing our own directives can significantly improve maintainability. This approach makes our Angular code more modular and easier to understand.
Expanding the host directives declaration
At some point, we will likely need to emit values from, or pass data to, a host directive. Instead of simply providing the hostDirectives
property with a list of standalone directive references, we can pass an expanded declaration of host directives that utilize input
and output
properties.
To illustrate this, let's expand on our previous dashboard case, where the DraggableWidgetDirective
now incorporates two class properties decorated with the input and output declarations:
As we see in this oversimplified example, the dashboard component template interacts with the input and output properties of the draggable directive through attribute and event binding.
Now that we have this basic parent-child data flow set up, let’s move on to the code compilation phase.
Compiling host directives
Host directives are statically applied to their corresponding hosts during compilation, not in runtime. During this process, the compiler verifies the host directive’s configuration, which must meet several requirements:
- Host directives should only reference classes annotated with a directive decorator.
- Referenced host directives should match an element only once, to prevent duplications.
- Referenced host directives must be standalone directives that do not require module declarations.
- Referenced host directives cannot be components, as components are subsets of directives with embedded template logic.
export interface HostDirectiveMeta {
directive: Reference<ClassDeclaration>;
isForwardReference: boolean;
inputs: {[publicName: string]: string}|null;
outputs: {[publicName: string]: string}|null;
}
The above interface HostDirectiveMeta
, defined in the compiler metadata source code, describes the properties of a host directive. These include a reference to the host directive class and the exposed inputs and outputs from the host directive.
This metadata interface also contains the isForwardReference
boolean property to indicate if the host directive is a forward reference. This can help the Angular compiler with rare cases of circular dependency when importing standalone directives. However, we can't access this property directly in the hostDirectives
entry: its primary function is during the compilation process when host directive information is parsed into the corresponding metadata.
Overcomposing as an anti-pattern
In a hypothetical situation with the dashboard example, it can be tempting to continue aggregating host directives to the WidgetComponent
to enhance the widget's capabilities.
Suppose our product owner request widget-based theming, widget responsiveness to the user's location, and the provision to toggle specific areas of the widget via a feature management system. Furthermore, the dashboard on mobile devices should have widgets to skew in 3D based on the device's positioning.
For suboptimal reasons, we find ourselves convinced that fulfilling all these requirements through the composition of multiple host directives is a viable approach.
Even before coding the tentative directives, it's evident that there is something wrong with the solution. Apart from the dubious approach to fulfilling the requested functionalities, the excessive use of host directives can significantly strain the browser resources, such as memory. With each widget instance, Angular creates not only an object of the WidgetComponent
class but also an instance of each host directive.
Just like any other aspect of frontend web development, Angular directive composition requires careful planning and monitoring to evaluate its impact on browser performance. By measuring the resulting complexity with Angular DevTools and performance with Lighthouse — or any other lab-testing tool — we can gain valuable insights into the consequences of clustering several host directives, each with its own specificities.
Conclusion
The Directive Composition API symbolizes how Angular continues to evolve and provides us with a powerful mechanism for reusing functionalities across different components and directives. But remember that excessive utilization of host directives could negatively impact the performance of web applications.
La versión en español de este artículo también se encuentra disponible en el blog de ng-conf: