Squeezing Angular Directives

Beyond the boundaries

Aleix Suau
Angular In Depth
Published in
7 min readMar 15, 2019

--

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

In this article, we’ll examine the features and limitations of every type of directive and how to mix all their features into one superdirective.

Starting from the basics, Angular has three types of directives: Attribute, Structural and Components.

1 — Attribute directives:

Change the appearance or behavior of an element, component, or another directive. Let’s see an example:

With Attribute directives we can modify the appearance or behavior of the Host element through:

  • @Input: Accessing the Host element properties.
  • @HostBinding: Setting properties to the Host element.
  • @HostListener: Listening to the Host element events.
  • @Output: Emitting events from the Host element.
  • ElementRef: Accessing the native DOM element (should be the last resource).

2 — Structural directives:

Change the DOM layout by adding and removing DOM elements (ie: *ngIf, *ngFor).

Are preceded by an asterisk (*) in their sugared syntax, but they become a <ng-template> wrapping the original content. Finally, this wrapper is rendered as a <! — comment — > in the DOM, placed right before the original content. Let’s see an example:

With Structural directives we can add/remove DOM elements thanks to:

  • ViewContainerRef:
    An Angular app is a tree of views (Host views (Components) or Embedded views (TemplateRefs)).
    In order to be shown, views need to be inserted into a ViewContainer:
    Host views are added to this tree with the ViewContainerRef.createComponent method, while Embedded views are added with the ViewContainerRef.createEmbeddedView method.
    In this example, the ViewContainerRef is the comment (<! — bindings={
    “ng-reflect-app-custom-if”: “true”} — >
    ).
  • TemplateRef:
    Represents the <ng-template> that wraps the original content, in this case the div (<div class=”name”>HOLA</div>) that is only inserted into the ViewContainerRef if the condition is truthy.

This template/comment nature of the structural directives is key to understand its limitations:

  • No @Output, no @HostListener, no @HostBinding:
    Right now we can’t listen or emit events from comments and we can’t set properties to comments either.
  • One @Input:
    Since the host element is a comment, we can’t access to its attributes, with an exception: the directive’s attribute (in this case appCustomIf). We will see that this is a special kind of @Input with superpowers (microsyntax).

3 — Components:

Finally, components are directives with a template. We have all the power of the attribute directives plus a template.

THE HELP-TOOLTIP DIRECTIVE

Let’s see the limitations of these three types of directives through an example. We want to build a directive with the following requirements:

1 — Adds a help hint bubble to the element. (?)

2 — Gets a dynamic text for the hint.

3 — Allows firing an action when the hint is hovered (ie: event).

Show me the code!
Ok, but after goes the explanation :)

Component

To get the job done, we could use a wrapper component with content projection (ng-content):

You can see all the code in the above Stackblitz (app > help-hint-component).

Ok, that works, but forces to wrap every element that needs a help hint with the <app-help-hint> tag, not practical. It is also semantically incorrect; app-help-hint takes all the protagonism when it is just a helper of the paragraph… Let’s try with an attribute directive.

Attribute Directive

We have seen that Attribute directives change the appearance or behavior of the host element so, by definition, we could not create new elements from it (help hint bubble).

Let’s cheat a bit by creating the help bubble element, attaching it to the DOM and setting a listener on it every time the value of the directive changes:

Use example:

You can play with the code in the above Stackblitz (app > help-hint-attribute-directive).

Ok, that works too, but at the cost of a lot of work. We have to take care of all the DOM manipulation and listeners every time the hint changes. We also have to remove the element when the host is destroyed… Let’s try with a structural directive.

Structural Directive

This is going to be a little tricky, so let’s break it down by requirements:

Requirement nº1: Adds a help hint bubble to the element
Structural directives can change the DOM layout through the ViewContainerRef and TemplateRef that they receive from its Injector.

It seems that there is no way to manipulate a TemplateRef (ie: appendChild the help hint bubble) so we’ll have to go with components. We will create a structural directive that dynamically loads a component in its ViewContainerRef.

This dynamic component will receive the TemplateRef as an @Input and will place it besides the help hint bubble, with the help of the ngTemplateOutlet directive. It also will accept the hint @Input and will emit a ‘helpHovered’ event when the user hovers the help bubble:

To integrate this dynamic component into the directive’s host element we will use its ViewContainerRef. ViewContainerRef has the createComponent method to add a Host view (component) from a component factory. Angular creates factories for every component that finds in templates and in the ‘entryComponents’ array of the ngModules, so we’ll have to add our new dynamic component to the ‘entryComponents’ array of the ngModule we are working on.

...
entryComponents: [HelpHintDynamicComponent]
...

Now we are ready to dynamically instantiate our HelpHintDynamicComponent from our structural directive, passing the TemplateRef to it:

Requirement 2: Gets a dynamic text for the hint.
We will use the same directive binding (appHelpHintStructural) to get the hint, and the same HelpHintDynamicComponent instance to pass it down:

Use case:

<p *appHelpHintStructural="hint"></p>

Requirement 3: Allows to fire an action when the hint is hovered.
Listen to hover events on the HelpHintDynamicComponent is as easy as adding the next line:

...
// Listen to hover event on the wrapper component
componentRef.instance.helpHovered.subscribe(() => ...);
...

But, this is a structural directive, it doesn’t have @Outputs so, how could the user of this directive react to the hover event?

To overcome this final challenge we will need the powers of the microsyntax. The Angular microsyntax lets you configure a directive in a compact string that allows template input variables and Angular expressions:

<p *appHelpHintStructural="hint; hovered: onHovered"></p>

Here we pass the hint as a first argument, followed by the function declaration that will be fired when the hint is hovered. This function declaration is bound to the directive with the appHelpHintStructuralHovered name (directiveName + functionName) so we’ll have to declare it as an @Input before:

...
@Input()
appHelpHintStructuralHovered
: Function;
...
// Listen to hover event on the dynamic component
// And fire the hovered input function
this.componentRef.instance.helpHovered.subscribe(() => this.appHelpHintStructuralHovered());
...

You can play with the code in the above Stackblitz (app > help-hint-structural-directive).

Final Cautions

‘Bound to the directive’ means that the function declaration will be called in the context of the directive (this === HelpHintStructuralDirective), not in the context where the method was declared (the component where the directive is used). So if the function declaration tries to access any property or method of the component, it won’t find it. Yes, the classical nightmare where ‘this’ is not this ‘this’.

We can solve this by binding the function declaration to the correct ‘this’ right in the directive:

...
this.componentRef.instance.helpHovered.subscribe(() => this.appHelpHintStructuralHovered.bind(this.viewContainer._view.component)())
...

Or by delegating this responsibility to the user that should declare these methods in the component with an arrow function:

...
onHovered = () => {
this.helpHoveredStructural = true;
}
...

Finally, keep in mind that ngOnChanges is never called in dynamically added components and don’t forget to destroy them when the directive is destroyed:

...
ngOnDestroy() {
this.componentRef.destroy();
}
...

And that’s it. We have bypass the ‘limitations’ of directives, mixing features from the three types!

CONCLUSIONS

Structural directives allow to dynamically instantiate components, getting the best from components (template) and from “directives” (flexibility). Ok but, when should I use each of them?

Well, not 100% sure, but I would say:

Components:
Use them when you want a standalone piece of functionality.

Attribute directives:
Use them when you want to extend the behavior of an existing element (@Output and @Hostlistener) or modify its appearance in easy ways (@HostBinding, ElementRef). For complex DOM manipulations, like adding and removing other elements, it gets tricky really fast.

You can also use them to add dynamic components (with ViewContainerRef) but they will be added as siblings of the host element, so the layout is very limited.

Here you can see an example.

Structural directives:
Use them when you want to extend the appearance of an existing element in a complex way, like adding and removing elements or even components.

When facing complex DOM manipulations, consider to create another component and inject it dynamically. Structural directives are the only ones that give us access to ViewContainerRef (point of insertion) and to the TemplateRef (host element), so we can rely on the power and simplicity of Angular components to solve complex DOM manipulations. Then we just have to inject the new component into the ViewContainerRef.

This is the case of our 4th option, “Structural directive with Material ToolTip and Material Icon” that uses the Tooltip directive and the Icon component from the Angular Material library to create a more elegant, simple and maintainable help hint solution (You can play with the code in the above Stackblitz (app > matx-help-hint)). Imagine if we would have to develop all this functionality by ourselves… We have saved a lot of work!

What do you think? How would you develop this ‘help-hint’ functionality?
Please comment below.

I work as a freelance Angular developer and trainer for startups and big companies. Do you need help with your projects? Don’t hesitate to contact me.

REFERENCES:

Really good post to wake up your Angular creativity and learn a lot about structural directives with Christian Janker:

Dynamic components made easy by the master Max Koretskyi aka Wizard:

Thanks to Max Koretskyi aka Wizard and Christian Janker for their review and advice.

You have learned something new, you deserve a gift:

--

--