Superpowers with Directives and Dependency Injection: Part 4
Tags: angular, javascript, typescript
Published: 2023-03-27 08:24:29

_Original cover photo by [Justin Wolff](https://unsplash.com/@jayphoto) on Unsplash._

Welcome to the fourth part of my exploration of Angular directives! We have explored [directive usage in template-local logic](https://dev.to/this-is-angular/superpowers-with-directives-and-dependency-injection-part-1-ho7), [structural directives instead of components](https://dev.to/this-is-angular/superpowers-with-directives-and-dependency-injection-part-2-16ea), and [using directives to extend the functionality of existing components and/or elements](https://dev.to/this-is-angular/superpowers-with-directives-and-dependency-injection-part-3-18ja). This time around, we are going to find out how directives can be used to work with events and add events to components that do not *really* exist.

Let's get started with two interesting use cases!

## A click-away directive

Sometimes we need to know when the user clicked outside of a given element. This is a common use case for dropdowns, modals, and other components that need to be closed when the user clicks outside of them. This can also be useful in a gaming app, or an app that shows videos (clicking away pauses the video, etc). We could do something inside of the component that has this functionality, but that would not be a very reusable piece of logic, considering we might need something like that in other components too. So, let's build a directive for this!

It is going to:

- take the target element
- inject the `Renderer2` instance to be able to listen to events
- listen to all click events on the document element
- if the target is not a descendant of the clicked element (thus being outside of it), emit an event
- dispose of the event listener when the directive is destroyed

Here is our implementation: 

```typescript
@Directive({
  selector: '[clickAway]',
  standalone: true,
})
export class ClickOutsideDirective implements OnInit, OnDestroy {
  private readonly elRef: ElementRef<HTMLElement> = inject(
    ElementRef,
  );
  private readonly renderer = inject(Renderer2);
  private readonly document = inject(DOCUMENT);

  @Output() clickAway = new EventEmitter<void>();
  dispose: () => void;

  ngOnInit() {
    this.dispose = this.renderer.listen(
      this.document.body,
      'click',
      (event: MouseEvent) => {
        if (!this.elRef.nativeElement.contains(
          event.target as HTMLElement
        )) {
          this.clickAway.emit();
        }
      }
    );
  }

  ngOnDestroy() {
    this.dispose();
  }
}
```

As you can see, the logic is very straightforward. We inject the `ElementRef` instance to get the target element, the `Renderer2` instance to be able to listen to events, and the `DOCUMENT` token to get the document element. We then listen to all click events on the document element, and if the target is not a descendant of the clicked element (we check it using the [`Node.contains`](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains) method), we emit a `clickAway` event. We then dispose of the event listener in `ngOnDestroy`.

Now, the cool thing about naming the `EventEmitter` the same as the directive selector is that we can just add this custom event on any element we like:

```html
<div (clickAway)="onOutsideClick()">
  <h1>Click outside of me!</h1>
</div>
```

Works like magic, as if it were a native event like `(click)` or `(mouseover)`!

Here is a working example with a preview:

{% embed https://stackblitz.com/edit/angular-twg9zi?ctl=1&embed=1&file=src/main.ts %}

Now, on to our next example.

## Handling scrolling 

Now, an often guest in different web apps is the ability to perform actions (like loading more content) when certain elements become visible in the view. This is a common use case for infinite scrolling, lazy loading, and other similar features. Again, as in the previous example, let us explore solutions that would allow us to reuse this logic in multiple places. Essentially, what we want is to have a custom event that would fire as soon as the element enters the viewport. 

We are going to use an [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for this, which allows observing whether a given element is intersecting with (essentially being visible inside) another element or the entire viewport. This will be a simplified example (lots of nuances can be present based on how exactly we want this to work), but the basic is that we can 

- take a target element
- listen to all of its intersection changes
- If it is interesecting with the viewport, emit an event

Now, here is an implementation:

```typescript
@Directive({
  selector: '[scrollIntoView]',
  standalone: true,
})
export class ScrollIntoViewDirective implements OnInit, OnDestroy {
  @Input() threshold = 0.25;
  @Output() scrollIntoView = new EventEmitter<void>();
  elRef = inject(ElementRef);
  observer: IntersectionObserver;

  ngOnInit() {
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            this.scrollIntoView.emit();
          }
        });
      },
      { threshold: this.threshold }
    );

    this.observer.observe(this.elRef.nativeElement);
  }

  ngOnDestroy() {
    this.observer.disconnect();
  }
}
```

Here, we create an `IntersectionObserver` instance, and we listen to all of its changes. If the target element is intersecting with the viewport, we emit a `scrollIntoView` event. We then disconnect the observer in `ngOnDestroy`. As you can see, the implementation is fairly similar to what we did with the `ClickAway` directive, the difference being the "business logic". Here is how we can use it in a template:

```html
<div>
  Really long content goes here
  <div (scrollIntoView)="loadMoreContent()">
    Dynamic content goes here
  </div>
</div>
```

Again, works like magic as any other native event!

Here is the preview: 

{% embed https://stackblitz.com/edit/angular-d8bq2d?ctl=1&embed=1&file=src/main.ts %}

> Note: the example shows a long list of `div`-s, scroll to the bottom to see a text message being logged into the console.


## Conclusion

As our exploration goes on, we learn more and more interesting use cases for Angular directives. In the next one, we will learn how to show templates outside of our components using directives and the concept of Portals. Stay tuned!