thumbnail

v-for指定した要素のテンプレート参照方法【Vue3】

Vue.jsにおいてv-forでHTML要素を繰り返し作成したとき、それぞれの要素に対してテンプレート参照する方法を解説します。ただしVue3のComposition APIのみを対象としています。また、要素の生成から参照するまでのタイミングや挙動、watchとの利用や注意点についてもまとめています。

前提

Vue.jsの3.x系でComposition APIを利用しています。

v-for指定した要素をテンプレート参照する方法

①<template>側でv-forを付けた要素に:refをつける

refではなく、:refのように「:」がついていることに注意して下さい。
そして:refの右辺に関数を代入します。
この関数は次の節できちんと定義するので、この時点で関数の名前は何でも構いません。

 <div v-for="i in num" :ref="setDivRef">{{i}}</div>

上のコードの例において、numは要素を繰り返す回数を表すとします。任意の数字に置き換えて下さい。

②<script>側で値の代入を行う

テンプレート参照をする配列を用意します。
そして①で指定した関数を定義し、その関数の中で引数を配列にpushします。

const divs = ref<HTMLDivElement[]>([])
const setDivRef = (el: any) => {
  if (el) {
    divs.value.push(el)
  }
}

これで配列divsの任意のインデックスにアクセスすれば、v-forで生成された各要素が参照ができます。
例えば、divs.value[0]にアクセスすれば1個目のdiv、divs.value[1]にアクセスすれば2個目のdiv、といった要領です。

また、①と②をまとめて次のようにも書けます。

 <div v-for="(n, i) in num" :ref="(el: any) => {if (el) divs[i] = el}">

v-forで繰り返される値が後から増えても、テンプレート参照が働いて配列としてアクセスすることができます。

【重要】DOMに更新がある場合

画面に更新処理が走る度に、テンプレート参照を格納した配列に参照がpushされてしまいます。
v-forとは関係のない部分のDOMの更新であっても、テンプレート参照の配列に同じテンプレート参照が何度も何度もpushされてしまい、意図したものが得られなくなります。
ですので、次のようなコードを書いて更新処理が走る前にテンプレート参照の配列を一旦空にする必要があります

onBeforeUpdate(() => {
  divs.value = []
})

テンプレート参照とマウント、watchのタイミング

以下v-forを5回繰り返して生成した要素を考え、テンプレート参照が行われるタイミングや動作などを見ていきます。

要素とコードが紐づくタイミング

要素はbeforeMountの後にコードと紐付きます。

const setDivRef = (el: any) => {
  if (el) {
    divs.value.push(el)
    console.log("push")//v-forは5回繰り返す
  }
}

console.log('created')
onBeforeMount(() => console.log('beforeMount'))
onMounted(() => console.log('mounted'))

このコードの出力結果は次のようになります。

created
beforeMount
push
push
push
push
push
mounted

確かにbeforeMount〜mountedの間にテンプレート参照がpushされていることが分かります。

したがって、setup直下にコードを書いても(createdのタイミングなので)テンプレート参照した要素にはアクセスできません。

//v-forは5回繰り返すのでdivsには5個要素が入るが
const divs = ref<HTMLElement[]>([])
console.log('created', divs.value.length) // 0になってしまう

マウント後なら次のコードのようにきちんとアクセスできます。

onMounted(() => console.log('mounted', divs.value.length)) //5

ウォッチを用いる場合

テンプレート参照をwatchEffectで監視するケースを考えます。

watchEffectをそのまま利用すると、テンプレート参照がpushされる度にウォッチが作動します。
また、初回(生成時)と2回目以降(push時)の実行タイミングも異なります。初回はcreated後、2回目以降はbeforeMount後です。

console.log('created',divs.value.length)
onBeforeMount(() => console.log('before mount',divs.value.length))
onMounted(() => console.log('mounted', divs.value.length))
watchEffect(() => console.log("watchEffect", divs.value.length))

このコードの出力は次のとおりです。

created 0
watchEffect 0
before mount 0
watchEffect 1
watchEffect 2
watchEffect 3
watchEffect 4
watchEffect 5
mounted 5

watchEffectのflushのタイミングをpostにすると、mountedの直前に1回のみ呼ばれるようになります。

console.log('created',divs.value.length)
onBeforeMount(() => console.log('before mount',divs.value.length))
onMounted(() => console.log('mounted', divs.value.length))
watchEffect(() => console.log("watchEffect", divs.value.length), {flush: 'post'})

このコードの出力は次のとおりです。

created 0
before mount 0
watchEffect 5
mounted 5

確かに、mounted直前に1回だけ呼び出されていることが分かります。

まとめ

v-forを指定した要素にテンプレート参照をする方法と、使用の際の注意点についてまとめました。
実行タイミングやDOM更新時の注意点には特に気をつけて利用する必要があります。