Free discovery callFree discovery call

How to implement Intersection Observer API in Angular

Loading many assets or a really long list of data can have a big performance impact on your page loading times. Learn how to implement the Intersection Observer API using Angular and RxJS and supercharge your application.

DevelopmentLast updated: 13 Dec 20243 min read

By Matej Jakšić

Nowadays, web applications are becoming more and more complex. Thus, one of the most important factors becomes the performance of the site. The Intersection observer API  provides a way to asynchronously observe changes of two elements in relation to each other. For example, a target element with an ancestor element or with the viewport. Because of that, it provides a variety of different use-cases that developers can use to create more efficient and performant web applications.

Use-cases

Why would you want to use this API? Intersection information can be very useful for many things such as:

  • Improving performance by only loading components that are visible on the screen
  • Lazy-loading images or lists of content that requires HTTP requests to be rendered
  • Implementing parallax effects where more content is loaded as you scroll
  • Calculating ad revenue based on ad visibility
  • Performing tasks or animations based on whether or not the user will see the result

Creating an intersection observer

Using the example taken from MDN, here’s an example of how to create intersection observer.

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

As we can create an intersection observer by calling its constructor and passing it a callback function and the options object.

Options object contains the following fields:

  • root - element that is used as the viewport for checking the visibility of the target. If not specified it defaults to page viewport
  • rootMargin - takes a value that will be used to calculate how much margin from the root an element needs to have to be considered “visible”. For example, we can start loading an image before it is fully visible to minimize the time needed to load it
  • threshold - number inside the interval [0,1] which defines at what percentage of the target’s visibility should the observer’s callback be executed, the default is 0 (meaning as soon as even one pixel is visible, the callback will be run), while 1 means that the threshold isn't considered passed until every pixel is visible

Integrating the Intersection Observer API with Angular

As we want to change the appearance/behaviour of DOM elements, we can make an Angular directive that can be used on these elements. To expose public API from directive we will use exportAs property inside directive decorator.

@Directive({
selector: '[appObserveElement]',
exportAs: 'intersection',
})

After that we need to define necessary Intersection Observer options we will use @Input() and @Output decorators. In addition to that, we will also define a few extra properties.

  • debounceTime - tells us when the user has stopped scrolling
  • isContinuous - flag that we use to decide whether we want to continue observing an element for visibility changes
  • isIntersecting - emits an event that tells us if the element is intersecting
  • intersecting - a public property that we use to trigger changes to the template

@Input() root: HTMLElement | null = null
@Input() rootMargin = '0px 0px 0px 0px'
@Input() threshold = 0
@Input() debounceTime = 250
@Input() isContinuous = false

@Output() isIntersecting = new EventEmitter<boolean>()

intersecting = false

Since we are using Typescript, we can define our options object as type IntersectionObserverInit.

After that, we can finally create our Intersection Observer.

createAndObserve () {
    const options: IntersectionObserverInit = {
      root: this.root,
      rootMargin: this.rootMargin,
      threshold: this.threshold,
    }

    return new Observable<boolean>(subscriber => {
      const intersectionObserver = new IntersectionObserver(entries => {
        const { isIntersecting } = entries[0]
        subscriber.next(isIntersecting)

        isIntersecting &&
          !this.isContinuous &&
          intersectionObserver.disconnect()
      }, options)

      intersectionObserver.observe(this.element.nativeElement)

      return {
        unsubscribe () {
          intersectionObserver.disconnect()
        },
      }
    })
      .pipe(debounceTime(this.debounceTime))
      .subscribe(status => {
        this.isIntersecting.emit(status)
        this._isIntersecting = status
      })
  }

In this method, we create an observer and wrap it inside of observable. Inside this observable, we define an unsubscribe() function where we disconnect from the Intersection Observer observable. Then we use a pipe function with debounceTime operator and subscribe to the changes and do our main status-changing logic. After that, all that is left to do is unsubscribe from the observable inside ngOnDestroy() lifecycle method.

Applying our directive

To use the appObserveElement, we add it to the HTML template with the directive name as an attribute. Then we pass our properties if we want to change some default value. Now we can use our directive reference to access our _isIntersecting property or we can use our isIntersecting() event to do some logic.

<mat-card
        class="example-card"
        *ngFor="let item of [].constructor(5); index as index"
        appObserveElement
        #intersection="intersection"
        [class.example-card--is-intersecting]="intersection._isIntersecting"
        (isIntersecting)="isIntersecting($event, index)"
        [isContinuous]="true"
>

The complete source code can be found at: https://github1s.com/matejjaksic/intersection-observer

To conclude

In this article, we’ve learned what is Intersection Observer and how we can apply it inside Angular. We have created a simple directive that can be reusable and is very versatile because we can change many properties with our @Input decorators. In the example, we can see how we can use our exportAs property to expose our directive inside the template. We’ve also implemented an emitter that we can use to do some logic inside our main component.

Thank you for your the time to read this blog! Feel free to share your thoughts about this topic and drop us an email at hello@prototyp.digital.

Related ArticlesTechnology x Design

View all articlesView all articles
( 01 )Get started

Start a Project