Intersection Observer APIを使ったVueのカスタムディレクティブ
はじめまして。
株式会社ココナラ フロントエンド開発グループのいっちーです。
フロントエンド開発グループの投稿としては最初のブログとなるでしょうか。
フロントエンドの開発をしていると、特定の要素がビューポートに入ってきた際に画像の読み込みの開始やAPIの呼び出しなど何かしらの処理を実行したいケースがしばしば出てくるかと思います。
そんなときに利用されるWeb APIとしてIntersection Observer APIがありますが、それを利用したVueのカスタムディレクティブの実装例をご紹介します。
Intersection Observer APIとは
MDNによれば以下のように説明されています。
交差オブザーバー API (Intersection Observer API) は、ターゲットとなる要素が、祖先要素または文書の最上位のビューポートと交差する変化を非同期的に監視する方法を提供します。
端的にいうと、対象となる要素がオプションで指定された祖先要素やビューポートが占める領域への出入りを検知するためのAPIということになります。
(本記事では簡略化のためビューポートの前提で説明いたします。)
カスタムディレクティブとは
Vue.jsではv-if
、v-for
などの標準ディレクティブの他に直接DOMに作用するようなカスタムディレクティブを定義することが可能です。
カスタムディレクティブの完成形イメージ
以下が今回作成するカスタムディレクティブの完成系イメージです。
<template>
<div
v-intersect
@intersect="onIntersect"
>
Element
</div>
</template>
<script lang="ts">
export default {
setup() {
const onIntersect = () => {
console.log('ビューポート内に入りました')
}
return {
onIntersect
}
}
}
</script>
v-intersect
が作成したカスタムディレクティブで、これを指定することで要素がビューポートへ入ってきた際に@intersect
イベントを発火できるようになります。
また、下記のようなケースにも対応できるように修飾子や値の指定ができるようにもしてみたいと思います。
- イベントが1度だけ発火するようにしたい
- ビューポートから出たときにもイベント発火するようにしたい
- ビューポートから出たときだけイベント発火するようにしたい
- 要素がビューポートに入っている領域の割合によってイベント発火のタイミングを設定したい
カスタムディレクティブの実装
以下がカスタムディレクティブ実装の全容になります。
import Vue, { VNode } from 'vue'
/**
* intersectイベント発火
* @param vnode 対象要素の仮想ノード
* @param entry 対象要素の交差状態
*/
const emitIntersectEvent = (vnode: VNode, entry: IntersectionObserverEntry) => {
const handlers = vnode.data?.on
if (handlers && 'intersect' in handlers) {
const handler = handlers['intersect']
if (typeof handler === 'function') {
handler(entry)
} else {
handler.forEach(f => f(entry))
}
}
}
/**
* 要素の監視解除
* @param el 対象要素
*/
const disconnect = (el: HTMLElement) => {
const id = el.dataset.intersectionObserverId
if (id) {
observers[id]?.disconnect()
delete observers[id]
}
el.removeAttribute('data-intersection-observer-id')
}
/** IntersectionObserverインスタンスの管理オブジェクト */
const observers: { [key: string]: IntersectionObserver } = {}
let currentId = 0
Vue.directive('intersect', {
inserted(el, binding, vnode) {
const { once, each, out } = binding.modifiers
const observerOptions: IntersectionObserverInit = typeof binding.value === 'object' ? binding.value : {}
/** イベント発火判定 */
const isFiring = (entry: IntersectionObserverEntry) => {
if (each) return true
if (out) {
return !entry.isIntersecting
}
return entry.isIntersecting
}
// IntersectionObserverインスタンスの生成
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (isFiring(entry)) {
// イベント発火
emitIntersectEvent(vnode, entry)
if (once) {
disconnect(el)
}
}
})
}, observerOptions)
// IntersectionObserverインスタンスの管理
const id = String(currentId++)
observers[id] = observer
el.dataset.intersectionObserverId = id
// 対象要素の監視
observer.observe(el)
},
unbind(el) {
disconnect(el)
}
})
insertedフックの実装
inserted
フックではIntersectionObserver
インスタンスの生成と要素の監視を開始します。
まず初めにディレクティブに指定された修飾子と値を取得します。
once
、each
、out
を指定することが可能でそれぞれ下記のような機能を持ちます。
修飾子 | 機能 |
---|---|
once | 1度だけイベントを発火する |
each | ビューポートの領域へ入ったときと出たときの両方イベントを発火する |
out | ビューポートの領域から出たときのみイベントを発火する |
each
およびout
の指定がない場合はデフォルトでビューポートへ入ったときのみイベントを発火します。
ディレクティブの値にはIntersectionObserver
コンストラクタの第2引数であるオプションを渡せるようになっており、イベント発火のタイミングを「要素の半分がビューポートに入ったとき」や「要素がビューポートに入る50px手前」などにコントロールすることができます。
次にIntersectionObserver
のインスタンスを生成します。コールバック関数内ではemitIntersectEvent
関数を実行してintersect
イベントに登録されたイベントハンドラを実行しています。
イベントハンドラには引数としてIntersectionObserverEntry
を渡すことでイベントハンドラ側でその時の要素の交差状態を受け取れるようになっています。
最後にIntersectionObserver.observe
で要素の監視を開始します。
このとき事前にIntersectionObserver
インスタンスを管理オブジェクトに追加し、払い出したIDを要素のdata属性に設定しておきます。これはunbind
フックの際に要素を監視対象から外すのにIntersectionObserver
インスタンスを紐付けしておく必要があるためです。
unbindフックの実装
カスタムディレクティブのunbind
フックではdisconnect
関数を実行して要素の監視解除を行います。
カスタムディレクティブの使用例
作成したカスタムディレクティブの使用例をいくつかご紹介します。
修飾子、値を指定しない場合
<template>
<div
v-intersect
@intersect="onIntersect"
>
Element
</div>
</template>
<script lang="ts">
export default {
setup() {
const onIntersect = () => {
console.log('ビューポート内に入りました')
}
return {
onIntersect
}
}
}
</script>
要素がビューポート内に入ったときのみイベント発火します。
v-intersect
ディレクティブと@intersect
イベントへのイベントハンドラを指定するだけのシンプルな実装となっています。
イベントを1回のみ発火させる場合
<template>
<div
v-intersect.once
@intersect="onIntersect"
>
Element
</div>
</template>
<script lang="ts">
export default {
setup() {
let count = 0
const onIntersect = () => {
// 2回目以降は呼ばれることがない
console.log(`${++count}回ビューポート内に入りました`)
}
return {
onIntersect
}
}
}
</script>
once
修飾子を付加して1度だけイベントが発火するようにしており、onIntersect
は2回目以降は呼ばれることがありません。
ビューポートに入ったときと出たときの両方イベント発火させる場合
<template>
<div
v-intersect.each
@intersect="onIntersect"
>
Element
</div>
</template>
<script lang="ts">
export default {
setup() {
const onIntersect = (entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
console.log('ビューポート内に入りました')
} else {
console.log('ビューポート外に出ました')
}
}
return {
onIntersect
}
}
}
</script>
each
修飾子を付加してビューポートに入ったときと出たときの両方でイベント発火するようにしています。
イベントハンドラは引数として受け取ったIntersectionObserverEntry
から要素がビューポート内外のどちらにあるのかが判別できます。
要素の50%がビューポートに入ったときにイベント発火させる場合
<template>
<div
v-intersect="{ threshold: 0.5 }"
@intersect="onIntersect"
>
Element
</div>
</template>
<script lang="ts">
export default {
setup() {
const onIntersect = (entry: IntersectionObserverEntry) => {
const ratio = entry.intersectionRatio
console.log(`要素の${ratio * 100}%がビューポート内に入りました`) // -> 要素の50%がビューポート内に入りました
}
return {
onIntersect
}
}
}
</script>
ディレクティブの値にIntersectionObserver
コンストラクタのオプションを指定しています。
ここでは{ threshold: 0.5 }
を指定することで要素の50%がビューポートに入ったときにイベント発火するようにしています。
IntersectionObserverEntry.intersectionRatio
から要素がどれくらいの割合でビューポートと交差しているかを取得できます。
最後に
今回はIntersection Observer APIを使ったVueのカスタムディレクティブ
の実装例についてご紹介いたしました。
このようにWeb APIを使って共通のVueのカスタムディレクティブを作成することで開発の効率化を図ることができるかと思います。
本記事がVue.jsで開発されている方のご参考になれば幸いです。
本記事をご覧になって興味を持たれた方、もっと詳しい話を聞いてみたい方はぜひ下記のカジュアル面談応募フォームよりご応募ください!
募集求人についてはこちらからご確認ください。
私の所属するフロントエンド開発グループでもエンジニアを募集中です!ご応募お待ちしております!
Discussion