0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Angular] imgタグやiframeタグのloading="lazy"のように、表示時にコンテンツを読み込みたい

Posted at

はじめに

imgタグやiframeタグには、表示されるまで読み込みを遅らせる機能 があります。

Angular のコンポーネントでも、imgタグやiframeタグと同じように、表示されるまでコンテンツの読み込みを遅らせたいです。

図にすると、こんな感じです。ビューポート内のコンポーネントはコンテンツを読み込んで表示します。ビューポートを外れているコンポーネントは、必要あるまでコンテンツを読み込みません。

Untitled.png

使い方というか設計方針

HTMLエレメントに、lazyLoad という属性を生やします。

lazyLoad 属性には、実際にコンテンツを読み込む処理を記述させます。

実装例
<div (lazyLoad)="onLazyLoad()"></div>

実装

なにはともあれディレクティブ

lazyLoad という属性を作りますので、なにはともあれディレクティブにします。処理内容を受け取れるように、@Outputをつけて、EventEmitterにします。

@Directive({
  selector: '[lazyLoad]',
  standalone: true,
})
export class LazyLoadDirective {
  @Output()
  public lazyLoad = new EventEmitter();
}

さて、あとはこのEventEmitterを適切なタイミングで発火させてやれば良いわけです。

やっぱりscrollイベントかな

ビューポートの外にいるエレメントがビューポート内に入るのを検出するイベントとしては、scroll (しかない?)でしょうか。

scroll イベントを受け取るサービスを作ります。

@Injectable({
  providedIn: 'root',
})
export class ScrollEventListenerService {
  public readonly scrolled: Observable<Event> = new Observable((observer) => {
    this.ngZone.runOutsideAngular(() => {
      // スクロール開始直後はスキップし、
      // しばらくしてから発火する
      fromEvent(window, 'scroll').pipe(auditTime(250)).subscribe(observer);
    });
  });

  constructor(private ngZone: NgZone) {}
}

ngZone.runOutsideAngular を使用して、Angular に捕捉されないところでイベントリスナーを設定します。

runOutsideAngular を使わないと、scroll イベントが起きるたびに、Angular の変更検出機構が動きます(という理解でいますが、あっているでしょうか?)。

あと、scroll イベントはガンガン起きますので、auditTime関数を使用して間引くようにしました。

ディレクティブを修正

作成したサービスと、HTMLエレメントのビューポート内での位置を取得するため、作成したディレクティブへ渡すようにします。

export class LazyLoadDirective {
  constructor(
    private element: ElementRef<HTMLElement>,
    private scrollEventListenerService: ScrollEventListenerService,
  ) { }

エレメントがビューポート内にいるか?

エレメントがビューポート内にいるかどうかを真面目にチェックしようとすると、横スクロールのこととかも考えないといけませんが、ここでは簡易的にエレメントの上辺がビューポート内かどうかをチェックすることにしました。

エレメントの上辺は、getBoundingClientRect API から返ってくる top の値を使います。

ビューポートの縦サイズは、document.documentElement.clientHeight の値を使います。

  private isInViewPort(): boolean {
    const top = this.element.nativeElement.getBoundingClientRect().top;
    return top >= 0 && top <= document.documentElement.clientHeight;
  }

スクロールイベントをサブスクリブする

作成したサービスのscrolledをサブスクリブします。

  private subscription?: Subscription;

  private subscribeScrollEvent(): void {
    this.subscription = this.scrollEventListenerService.scrolled
      .pipe(first(() => this.isInViewPort()))
      .subscribe(() => {
        console.debug('Emit lazyLoad.');
        this.lazyLoad.emit();
      });
  }

サブスクリブしっぱなしだとまずいので、あとでアンサブスクリブできるように値を保存しておきます。

また、何度も読み込みイベントを起こすわけにもいかないので、first 関数で最初の1回に制限します。

ライフサイクルのどこでサブスクリブする?

Angularコンポーネントのライフサイクルフック は、いくつもあります。サブスクリブするのは1回限りでよいので、1回だけ起きるところがよいです。

候補は下記になります。

  • コンストラクタ
  • ngOnInit
  • ngAfterContentInit
  • ngAfterViewInit
  • afterNextRender

いつものように ngOnInit でよいかと思いましたが、その段階だとエレメントのレンダリングが終わっていないので、getBindingClientRect が 0 を返してきました。

エレメントの位置を取得するため、afterNextRender を使うことにしました。

コンストラクタの中で設定します。

  constructor(
    private element: ElementRef<HTMLElement>,
    private scrollEventListenerService: ScrollEventListenerService,
  ) {
    afterNextRender(() => {
      if (this.isInViewPort()) {
        console.debug('The content is in viewport, Emit lazyLoad.');
        this.lazyLoad.emit();
      } else {
        console.debug('Subscribe scroll event.');
        this.subscribeScrollEvent();
      }
    });
  }

サブスクリブするついでに、すでにエレメントがビューポート内にいるときは、イベントを発火するようにしました。

サブスクリブの後始末

コンポーネントがいなくなったあとも、scroll イベントをサブスクリブしっぱなしだとまずいので、破棄時にアンサブスクリブします。

OnDestroy を実装します。

export class LazyLoadDirective implements OnDestroy {
  ngOnDestroy(): void {
    console.debug('Unsubscribe scroll event.');

    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }

できあがったもの

Loading... の文字が置き換わっているのわかるでしょうか。

名称未設定.gif

ソース全体はこちらに置きました。

環境

最後に環境です。

  • Angular 18.2.0

参考リンク

runOutsideAngular の使い所についての解説です。

Angular Material の Virtual Scroll を参考にしました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?