Angular - Lazy ngFor when you only need N elements to start
Tags: webdev, angular, typescript, coding
Published: 2024-05-05 11:19:13

When we started writing [classifieds website - mamrzeczy.pl](mamrzeczy.pl), we wanted the main page with ads to display 40 ads per page. But we quickly noticed that lighthouse gave a very low performance score. We decided that we needed to somehow limit the amount of content loaded at the start of the page. And this is how the NgForLazyDirective was created.

The directive works in a very simple way.
1. Renders N elements and adds an IntersectionObserver on the last element.
2. When the user scrolls to the last element, it renders N elements again as in step 1.

I'm posting it because maybe someone will find such simple use useful, as the directive itself is not too invasive and does not require many changes to the design.

```typescript
import { NgFor, NgForOfContext } from '@angular/common';
import {
  ChangeDetectorRef,
  Directive,
  Input,
  IterableDiffers,
  NgIterable,
  OnChanges,
  SimpleChanges,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import { isPlatformBrowser } from './inject-functions/is-platform-browser';

@Directive({
  // eslint-disable-next-line @angular-eslint/directive-selector
  selector: '[ngFor][ngForLazy]',
  standalone: true,
})
export class NgForLazyDirective<T, U extends NgIterable<T> = NgIterable<T>>
  extends NgFor<T, U>
  implements OnChanges
{
  @Input() ngForLazy!: U & NgIterable<T>;
  @Input() ngForN: number | undefined | null;
  observer!: IntersectionObserver;
  end = 0;

  constructor(
    private viewContainer: ViewContainerRef,
    template: TemplateRef<NgForOfContext<T, U>>,
    differs: IterableDiffers,
    private cdr: ChangeDetectorRef,
  ) {
    super(viewContainer, template, differs);

    if (isPlatformBrowser()) {
      this.observer = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              this.observer.unobserve(entry.target);
              this.render();
            }
          });
          if (this.ngForN && this.end < Array.from(this.ngForLazy).length) {
            this.observer.observe(
              viewContainer.element.nativeElement.parentElement.lastChild
                .previousElementSibling as Element,
            );
          }
        },
        {
          rootMargin: '100px',
        },
      );
    }
  }

  isObserved = false;

  observe() {
    if (this.isObserved) {
      return;
    }
    const element = this.viewContainer.element.nativeElement.parentElement
      .lastChild.previousElementSibling as Element;
    if (this.observer && element) {
      this.observer.observe(element);
      this.isObserved = true;
    }
  }

  disconnect() {
    if (this.observer) {
      this.observer.disconnect();
      this.isObserved = false;
    }
  }

  getForOf() {
    return Array.from(this.ngForLazy).slice(0, this.end) as U & NgIterable<T>;
  }

  render() {
    if (!this.ngForN) {
      this.ngForOf = this.ngForLazy;
      this.disconnect();
      super.ngDoCheck();
      this.cdr.markForCheck();
      return;
    }
    this.end += this.ngForN;
    this.ngForOf = this.getForOf();
    this.observe();
    super.ngDoCheck();
    this.cdr.markForCheck();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if ('ngForLazy' in changes) {
      this.render();
    }
    if ('ngForN' in changes) {
      this.render();
    }
  }
}
```

Example of use in code:

```html
<div *ngFor="let item; lazy: items; trackBy: trackBySlug; n: 10">
  <a routerLink="/ad/{{ item.slug }}">
    <app-ads-item [classifiedAd]="item" />
  </a>
</div>
```

If you like this post, give it a heart. 💕
If you have a question, leave a comment, I will be happy to discuss. 💬