特定の要素が画面内に完全に表示されたことを検知する

# はじめに
こんにちは。
今回業務で、特定に要素が画面内に100%入ったことを検知してデータ更新するような作業を行った際に、フロント実装でInteraction Observer APIというものを使用したので備忘録として残しておこうと思います。

# 今回やったこと
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1462552/afe6a1e8-d4be-7a2b-eb6e-adc0bf000ccb.gif)



Interaction Observer API　というものを使用して、円の形の画像と名前が完全に画面に入ったということを監視してその要素の数だけカウントしていくということをやりました。

*Vue3とNuxt3を使用しています
*CSSはTailwind cssを使用しています


### Interaction Observer APIとは
交差オブザーバー API (Intersection Observer API) は、ターゲットとなる要素が、祖先要素または文書の最上位のビューポートと交差する変化を非同期的に監視する方法を提供します。(ドキュメントより)

https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API

# とりあえず使ってみる

#### 下準備で画像と名前を登録して表示するだけのものを実装

一旦、こんな感じで実装します。
```vuejs:index.vue
<template>
  <div class="grid grid-cols-5 gap-8 px-32 py-12 font-dotgothic font-bold">
    <p class="stay">{{ displayedItemsCount }}</p>
    <div
      v-for="(item, index) in items"
      :key="index"
      class="flex flex-col items-center"
      ref="observeItems"
    >
        <img
          class="rounded-full w-44 h-44 hover:ring-4 hover:ring-orange-500 hover:scale-110 transition-transform object-cover shadow-md"
          :src="item.image"
          alt="Image"
        />
      <p class="text-center mt-2">{{ item.name }}</p>
    </div>
  </div>
  <IndexUserPlus @submit="addItem" />
</template>

```

簡単なモーダルを作成して、imageと名前を保存します。
作成したデータがリロードしても消えないようにitems配列をJSON形式の文字列に変換し、それをlocalStorageに一旦保存してあります。
```vuejs:index.vue
<script>
export default {
  name: "IndexMain",
  data() {
    return {
      isModalOpen: false,
      form: {
        name: "",
        image: null,
      },
      items: JSON.parse(localStorage.getItem("items")) || [],
      displayedItemsCount: 0,
    };
  },
  methods: {
    addItem(item) {
      if (item && item.name && item.image) {
        const id = this.items.length + 1;

        item.id = id;

        this.items.push(item);
        localStorage.setItem("items", JSON.stringify(this.items));
      }
    },
  },
};
</script>
```

# 監視する

vueのmountedライフサイクルフックの中で、Interaction Observer APIを使用します。

具体的な方法として、今回は以下のようにします。
* データ変更がDOMに反映された後に実行するメソッドを作り、DOMが更新されてからInteraction Observerを使用する
* 特定のDOM要素が画面内に入ったかどうかを監視する
* Intersection Observerのコールバック関数を作り、監視対象の要素が画面内に入ったかを検知するたびに呼び出す
* 監視対象の要素が画面内に入ったとき、displayedItemsCountを増やし、その要素の監視を停止する
* 要素が画面内にどの程度入ったときにコールバックを呼び出すかを指定する

```vuejs:index.vue
<script>
//
//その他のコード
//
mounted() {
    this.$nextTick(() => {
      const observer = new IntersectionObserver(
        (entries, observer) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              this.displayedItemsCount++;
              observer.unobserve(entry.target);
            }
          });
        },
        {
          threshold: 1.0,
        }
      );

      if (this.$refs.observeItems) {
        this.$refs.observeItems.forEach((item) => {
          observer.observe(item);
        });
      }
    });
  },
  </script>
```

まず、Vue3のnextTickを使用して、データ変更がDOMに反映された後にIntersection Observerを設定します。

Vue.jsでは、データの変更が非同期にDOMに反映されます。つまり、データを変更した直後にDOMを操作しようとすると、変更がまだ反映されていない可能性があるので、vueが提供しているthis.$nextTick()というメソッドを使用します。　

状態を変更した直後に nextTick() を使用すると、DOM 更新が完了するのを待つことができます。(ドキュメント参照)
```vuejs:index.vue
mounted() {
    this.$nextTick(() => {...})
}
```
https://ja.vuejs.org/api/general

引数としてコールバックを渡すか、戻り値の Promise を使用できるとのことなので、コールバック関数を引数として
新しいIntersection Observerを作成し、それをobserveItemsという名前のrefが参照する各DOM要素に対して設定します。

```vuejs:index.vue
 const observer = new IntersectionObserver(
        (entries, observer) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              this.displayedItemsCount++;
              observer.unobserve(entry.target);
            }
          });
        },
        {
          threshold: 1.0,
        }
      );
      
```

```
threshold: 1.0 //ここで要素がどれだけ画面内に入ったかどうかを判定する。今回は100%で指定
```

#### IntersectionObserver
特定の要素がビューポート（ブラウザの表示領域）と交差（表示または非表示）したことを検知します

entries → 交差情報を持つオブジェクトの配列
observer → 現在のIntersection Observerのインスタンス
isIntersectingをチェックして、その項目が現在ルートと交差している要素を表しているかどうかを確認する
observer.unobserve(entry.target);：監視対象の要素の監視を停止する

```js
let callback = (entries, observer) => {
  entries.forEach((entry) => {
    // それぞれの項目は、観測された 1 つの対象要素の交差状態の変化を示している。
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};
```
ドキュメント参照


# Interaction Observer APIのメリット

#### パフォーマンス
Intersection Observer APIは非同期に動作していること

#### 簡単なAPI
要素が画面内に入ったかをチェックするコードを書く必要がないこと


# さいごに
たのしい。