Angular lazy loading directive with IntersectionObserver
Tags: javascript, angular, typescript, webdev
Published: 2022-06-02 12:27:27

@angular/cli: 13.3.0

{% github anirbmuk/ng-lazy-load no-readme %}

So let's dig into lazy loading once again! Previously, I had written an article [Angular infinite scrolling using RxJs and NgRx](https://dev.to/anirbmuk/angular-infinite-scrolling-using-rxjs-and-ngrx-4p8i), which explained the concept of loading more data on reaching the end-of page. But this time, the concept is wider, or more generic.

This time, I would like to reach a certain part of the viewport, and then load some data specific to that part, or maybe perform some tracking action. So I would observe a specific HTML selector and then do something when that selector comes into view. And that selector could be anywhere in the page, not just the end, something like this:

![Preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/di49yal8yt4eoc5gj3ho.gif)

So of course, we go for the **IntersectionObserver** API. Almost all modern browsers now support it, so we should ideally have no problem for a modern website.

Let us once again break down the requirements, one by one.
1) We need a _custom directive_, which we can place on any HTML element.
2) This directive will accept a callback method as a parameter, so that the parent component consuming the directive can decide what to do once the child element comes into view.
3) Tracking should happen only once, so that every time the element comes into view (due to user scrolling up and down), the data should not be re-fetched. _But this is my specific use-case, you may choose to do differently_.
4) The observer should be disconnected when the element is destroyed.

---

## The directive:
Whenever we place a directive on an element, we get a reference to the consuming element. And to be honest, this is all we need.
Almost the entire logic happens in the constructor, since each directive placed on each element creates a new instance of the class.

![constructor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sq1kclwiggq2gqn8z0o1.png)

Let's understand what is being done:

### The Options:
**root** - this essentially means the relative component against which your component - the one you would like to observe - should intersect. For our use case, this is the entire document.
**rootMargin** - this would consider if the document has some margin, so that the intersection logic would consider that during its calculation.
**threshold** - this is a value between 0 and 1 (or consider 0 and 100%). A value of 1 means, the API would consider my component to be intersecting with the document only when 100% of the element is in view. You can change this as per your need.

### The Intersection callback and The Observer:
For all components we observe, the callback is executed. For our use case, we have only one target - the element on which our directive is placed. We create a new instance of `IntersectionObserver` and assign it to an observer. This observer observes our component, which is injected into our directive class through `elementRef: ElementRef`.

### So what happens next?
Depending on the threshold value, the `entry.isIntersecting` evaluates to true or false, and it is then that we need to do something.

### And what do we do then?
Our directive takes in a callback as an input, and we fire this callback - `this.appTrackable?.();`

Also, I mentioned before that each directive on each element is a new instance. So we maintain a class level variable - tracked. And once the intersection logic is satisfied and the callback is fired, we do not need to fire the logic again and again. You could go one step ahead and disconnect the observer itself.

When the component gets destroyed, so does the directive, and you can tap onto the `ngOnDestroy` method, to disconnect the observer.

```
ngOnDestroy(): void {
  this.observer?.disconnect();
}
```

---

## The implementation
The implementation means placing the directive on an element and then passing a callback reference to it.

**home.component.html**
```
<app-people [appTrackable]="callback1"></app-people>
<app-place [appTrackable]="callback2"></app-place>
```

The callback methods include some logic to fire HTTP calls to fetch data for that element, but that is not what this article is about and so I will not go into its details. But you can always check it out from [GitHub](https://github.com/anirbmuk/ng-lazy-load). _Just remember to bind your callback to the service or class instance, as applicable._

**home.component.ts**
```
export class HomeComponent {
  constructor(private readonly data: DataService) {}

  readonly callback1 = this.data.callback.bind(this.data, 'people');
  readonly callback2 = this.data.callback.bind(this.data, 'places');
}
```

---

## So what are the possibilites?
Well you can do whatever you want with the callback thingy! But, I would say, tracking for analytics is one very important use case. You can also use this to execute infinite loading on scroll. Let me know in the comments if you can come up with more use cases!

Cheers :-)
[Anirban Mukherjee](https://anirbmuk.appspot.com/en)