jsJavaScript 関連の記事

Angularで無限スクロールを実装する

Angularで無限スクロールを実装する

コンテンツを次々自動的に読む混む無限スクロール。SNSやスマフォアプリでは当たり前に実装されていて目にする機会も増えました。今回はその無限スクロールをAngularを使って実装してみました。

よくブログ記事の一覧や画像一覧のページなどでページネーションを使用されているのを目にしたことがあると思います。そのようなページもページネーションではなく無限スクロールでコンテンツを追加した方が最適な場合もあります。

無限スクロールはページネーションと違いクリックせずにスクロールだけで操作が完了するのでターゲットユーザーやメインデバイスによっては UI/UX の向上も見込めます。特にスマートフォンとの相性はとてもいいと思います。

無限スクロールとは?

Facebook や Twitter のタイムラインを見ていると下の方に次々とコンテンツが表示されていくのを見たことがあると思います。 あの表示方法が無限スクロールと言われています。

技術的な仕組みとしては 特定の場所までスクロールしたら裏側(JavaScript)で API からコンテンツを取得し、整形して表示するとなります。

無限スクロールのメリットとデメリット

うまく組み込めば次のようなメリットも見込めます。PC よりスマートフォンのアクセスが多い今の時代に適した方法とも言えるでしょう。

メリット

  • タップやクリックをせずにスクロールだけ操作が完結するので快適
  • 次のページを読み込む待ち時間がないのでユーザーがコンテンツに集中しやすく、より長くサイトに留めておける可能性が高い
  • スマートフォンとの相性がいい

もちろん、無限スクロールも万能ではありません。次のようなデメリットもあります。

デメリット

  • コンテンツを次々読み込むため目的のコンテンツを過ぎた場合に遡って探すのが難しい
  • コンテンツが増減しても URL が変わらないためブックマークなどの保存アクションに向かない。
  • コンテンツを全て読み込むまでメインエリアの下(フッターなど)が表示されない
  • 後から読み込まれるコンテンツはアクセス初期は表示されないため SEO との相性があまり良くない

など、デメリットもありますので採用される際は気をつけましょう。

Angular での実装サンプル

前置きが長くなりましたが本題のAngular で無限スクロールを実装する 方法を紹介したいと思います(Angular を選んだことに特に意味はありません)

今回作るサンプルはこちらです。
ちょっと見えづらいですが、スクロールするとデベロッパーツールに posts?_start=X&_limit=Y が増えてるのがわかります。 これが裏側で API からコンテンツを取得している実際の動きになります。

無限スクロールでAPIからデータを取得するブラウザのGIF

サンプルのソースコードはこちら。 データの取得には JSONPlaceholder を利用させていただきました。

サンプルコードの解説

まず今回使用するモジュールをインポートしておきます。

ViewChild, ElementRef, HostListenerangular/coreから。 HttpClientangular/common/http からインポートします。

  • ViewChild, ElementRef
    HTML要素を参照するために使用します。
  • HostListener
    ウィンドウのスクロールイベントを検知しイベントを発火させるために使用します。
  • HttpClient
    非同期通信にてAPIからデータを取得する際に使用します。

ViewChildで要素を参照する

次に無限スクロールで読み込んだコンテンツを包括する要素を@ViewChildを使って参照できるようにします。 今回は、包括する要素は<div #content></div> になりますので下記のようになります。

※document.querySelectorなどでの要素の参照はAngularでは非推奨です。
@ViewChild('content', { static: false }) content!: ElementRef;

データ格納用の変数などを用意

今回はAPIから10件ずつデータを取得し、100件になるまで取得を繰り返します。
そのため、APIからのデータを格納する posts と posts内のデータの数を格納するpostsLengthを作成します。 そして、APIからのレスポンスに時間がかかってしまう場合も考慮してローディング用の変数isLoadingも用意しておきます。

isLoading: boolean = false;
posts: tPosts[] = [];
postsLength: number = 0;

APIからデータを取得する関数を定義

APIからデータを取得するために処理を関数にしておきます。
後にスクロールした時にこの関数を発火させて無限スクロールを実現します。何度も使用するので関数にして使いまわせるようにしておきましょう。

getPosts() {

  // ローディング中にする
  this.isLoading = true;

  // 100件以上になったら何もしない
  if (this.postsLength >= 100) {
    this.isLoading = false;
    return;
  }

  // APIからデータを取得しpostsに追加していく。何番目のデータから取得するかを _start=xx で設定する
  this.httpget<tPosts[]>(`https://jsonplaceholder.typicode.com/posts?_start=${this.postsLength}&_limit=10`).subscribe((data) => {

    this.posts = [...this.posts, ...data];
    this.postsLength = this.posts.length;

    this.isLoading = false;

  }, (error) => {

    console.log(error);
    this.isLoading = false;

  });
}

ウィンドウのスクロールを監視し関数を実行する

@HostListener を使ってウィンドウのスクロール監視し、<div #content>が存在していればイベント(scrollingSubscrib)を発火します。 この時点ではまだ 「どのくらいスクロールしたら次のデータを取得する」 などの制限していません。その制限はscrollingSubscrib内で行っています。

@HostListener('window:scroll', []) onWindowScroll() {
  // 対象の要素の存在をチェック
  if (this.content && this.content.nativeElement) {
    this.scrollingSubscrib();
  }
}

APIから続きのデータを取得する

APIから続きのデータを取得できるようにscrollingSubscrib()を作成します。
取得しにいくタイミングは<div #content>の一番下が画面内に表示されたら、getPosts()を実行しAPIから再度データを取得します。

scrollingSubscrib() {

  // ローディング中は実行しない
  if (this.isLoading) return;

  // this.contentで<div #content>を参照しgetBoundingClientRect()で幅や高さを取得
  const rect = this.content.nativeElement.getBoundingClientRect();

  // 発火地点を決める
  // window.innerHeight * 1 = ウィンドウの最下部に<div #content>>の一番下が来た時
  // window.innerHeight * 0.2 = ウィンドウの最下部に<div #content>の下から20%が画面に入った時
  const triggerPoint = rect.bottom - window.innerHeight * 1;

  // APIからデータを取得
  if (triggerPoint <= window.innerHeight) {
    this.getPosts();
  } else {
    return;
  }
}

まとめ

処理の流れとしてはそこまで複雑では無いので迷うことはないと思います。
生のJavaScriptだとちょっとめんどくさい処理も、Angularはあらかじめさまざまなモジュールが用意されているので簡単に作成できますね。
リファクタリングしてないのでちょっとコードが雑なところがありますがそこはご容赦ください。

便利な無限スクロールですが、表示するコンテンツやレイアウトによっては不向きな場合もありますので使用する際は 「無限スクロールでの表現がこの場合は最適か?」 を考えて採用するのをオススメします。

コピーしました

コピーに失敗しました