Angular – Directive Composition API

Hello to all anglers!

In this article, we will deal with the novelty from Angular 15 – Directive composition API.

I apologize for the late text, our corporate meetup took place a long time ago, but there wasn’t enough time to write something…

So, fasten your seat belts, we are starting to develop our angular velocity.

What problem are we solving?

  • Reduce code repetition when creating similar directives.

  • But the main problem is the expansion of the functionality of directives that are taken from libraries!
    There is a corresponding one in the angular repository proposaland it became very popular.

A bit of theory

  • About the pattern:
    Directive composition is very similar to the Linker pattern (Composite).
    It belongs to the family of Structural Programming Patterns (which are responsible for creating convenient objects).
    The composer helps to combine several objects into one, which can have a tree structure, but we will only access the composition according to a certain contract of the root of this tree, without thinking about how we can work with each sheet separately.

  • About standalone components:
    In Angular v14 we have standalone components. In 14, standalone components appeared in the preview, and in 15, they got a full-fledged place under the sun (if we don’t know about it, then we missed a lot and should run to read articles about standalone components).
    As we remember, directives differ from components in that they do not have a template … but the standalone property also exists. When we start talking about the directive composition API, the thing to remember is that directives must be declared as standalone.

  • About the host:
    In our case, this is a component / directive, on which we will attach other directives.

Let’s move on to practice

Let’s say we have an app-card component and we want to add drag and drop capability to its behavior, and we also want all of our app-cards to apply our super unique directive that paints the box’s background red…

What did we do before version 15?

It was necessary to add the necessary directives to each app-card component, and in all occurrences of our card we had to write <app-component RedBackgroundDirective CdkDrag>of course, we could create an additional wrapper component that would contain a card with directives, but this is not a very clean solution …

Now we can define inside the decorator which directives should be applied to the component, sounds convenient!

By the way, I haven’t touched pure js for a long time and thought that there decorators already implemented, but the proposal for them is on stage 3.

So let’s take a look at This

@Directive({
  selector: '[appRedBackground]',
  standalone: true,
  host: { '[style.background]': '"red"' }
})
export class RedBackgroundDirective {}

@Component({
  selector: 'app-card',
  template: '<ng-content/>',
  styles: [':host { display: block}'],
  hostDirectives: [
    RedBackgroundDirective,
    CdkDrag
  ]
})
export class CardComponent {}

It turned out that mixing / composing directives is very simple and no wrapper components are needed.

Great, but we can also wrap directives inside another directive.

For example, we have a text highlighting directive appHighlight

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective implements OnChanges {
  @Input('appHighlight') searchTerm: string | null="";
  @Input() caseSensitive = false;
  @Input() customClasses="";
  // some code
}

And also the directive for displaying the tooltip appTooltip

@Directive({
  selector: '[appTooltip]',
  standalone: true
})
export class TooltipDirective {
  @Input('appTooltip') text="";
  @Output() showTooltip = new EventEmitter<boolean>();
  // some code
}

And, of course, we want to create a directive that will select text and show the tooltip)

All we need is to create a directive appHighlightWithTooltip and do some work on its decorator.

@Directive({
  selector: '[appHighlightWithTooltip]',
  hostDirectives: [
    { 
      directive: HighlightDirective, 
      inputs: ['customClasses', 'appHighlight: highlight']
    },
    {
      directive: TooltipDirective,
      inputs: ['appTooltip: tooltip'],
	  outputs: ['showTooltip']
    }
  ]
})
export class HighlightWithTooltipDirective {}

Thus, we created a composition from directives, but changed something in the contract of the new object:
– We will not be able to access HighlightDirective.caseSensitive.

Since this Input was not declared in the decorator. (By default, all inputs and outputs do not flow to the top, here we see the whiteList concept. No omitonly hard pick).

– To appHighlight.appHighlight we will address not appHighlight, but highlight

An alias has been set inputs: ['appHighlight: highlight']

Life cycle

Very simply, all hooks are executed in the order in which the directives are set inside the decorator, after them the host hooks are processed.

Let’s go back to the first example

@Component({
  selector: 'app-card',
  template: '<ng-content/>',
  hostDirectives: [
    RedBackgroundDirective,
    CdkDrag
  ]
})
export class CardComponent {}

If you add to the directives and the component ngOnInit And ngAfterViewInit For example, the order would be:

1 RedBackgroundDirective - constructor
2 CdkDrag - constructor
3 CardComponent  - constructor
4 RedBackgroundDirective - ngOnInit
5 CdkDrag - ngOnInit
6 CardComponent  - ngOnInit
7 RedBackgroundDirective - ngAfterViewInit
8 CdkDrag - ngAfterViewInit
9 CardComponent  - ngAfterViewInit

Accessing directive properties

The question may arise, how to access the properties of the directive from the decorator from the host, for example, to override the default value for Input?

@Directive({
  selector: '[appHighlightWithTooltip]',
  hostDirectives: [
    { 
      directive: HighlightDirective, 
      inputs: ['appHighlight: highlight']
    }
  ]
})
export class HighlightWithTooltipDirective {
  constructor() {
    inject(HighlightDirective).caseSensitive = false;
  }
}

I want to note that all public properties are visible, no matter if they were defined in the decorator as visible or not.

Unsubscribe via directive

So, it seems that a new version of angular has been released, but we have not taken up the favorite pastime of any Angularist – search new way to unsubscribe…

@Directive({
  selector: '[appDestroy]',
  standalone: true
})
export class DestroyDirective {
  private _destroy$ = new Subject<boolean>();
  get destroy$(): Observable<boolean> {
    return this._destroy$.asObservable();
  }
  ngOnDestroy(): void {
    this._destroy$.next(true);
    this._destroy$.complete();
  }
}

@Component({
  selector: 'app-card',
  template: '<ng-content/>',
  hostDirectives: [DestroyDirective]
})
export class CardComponent {
  destroy$ = inject(DestroyDirective).destroy$; // takeUntil(this.destroy$) - и полетели
}

But you should definitely mention the main disadvantage of this approach – destroy$ will work at the time of the call ngOnDestroy inside the directive, and this will happen before the call ngOnDestroy for a component (see above about the life cycle).

Performance

The chain of directives can be quite large, and a large number of directives can be added to one host.

@Directive({
  selector: '[c]',
  hostDirectives: [ADirective, BDirective]
})
export class CDirective {}

@Directive({
  selector: '[e]',
  hostDirectives: [CDirective, DDirective ]
})
export class EDirective {}

Which can lead to performance problems, since for EDirective ngcc will create a separate object for each directive from the composition.

And if EDirective is applied to ngFor components with 100 elements, then 500 additional objects will be created, which will result in eating up memory.

In fact, this most likely will not affect the average PC, but with large amounts of data, you should not forget about the profiler =)

Conclusion

v15 has a great way to expand the functionality of library directives, as well as write small directives that can be combined in various combinations, which will increase dry and s from solid.

Link for code examples
– Documentation angular.io

Similar Posts

Leave a Reply

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