Angular Directive Composition

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

--

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:

A dashboard component renders a widget with two attribute directives in the traditional way.

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.

Angular directive providing resizing functionality.
Angular directive providing drag-and-drop behavior.

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.

Widget component with host directives applied.
The dashboard component now renders a widget that contains host directives.

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:

Draggable widget directive showcasing the input and output mechanism.
Widget component referencing the input and output properties from the draggable host directive.
Dashboard component interacting with the widget's draggable host directive.

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.

Widget component with so many host directives that it may affect frontend performance

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.

--

--

Alejandro Cuba Ruiz
ngconf

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