その他

【Vue.js】v-for内でテンプレート参照を使って面倒な仕様の罠にハマった話

この記事は約6分で読めます。

はじめに

Vue.jsでテンプレート参照を使うのにやっかいな仕様に気づかずに少し詰まったことがあったので忘備録としてまとめようと思います。

テンプレート参照とは

まず、テンプレート参照とは特定の DOM 要素や子コンポーネントに直接アクセスして操作する機能です。

例えば以下のコードのようにinput要素にrefを設定するとrefの要素の値を取得したりフォーカスを当てたりすることが出来ます。

<script setup>
import { ref, onMounted } from "vue";

const inputRef = ref();

const onClick = () => {
  console.log(inputRef.value.value);
  inputRef.value.focus();
};
</script>

<template>
  <input ref="inputRef" value="テンプレート参照" />
  <button @click="onClick">フォーカス</button>
</template>

v-for内でテンプレート参照を使うときに引っかかる罠

v-for内でrefを使用すると、対応する参照には配列値が格納されます。そしてこの配列値には、マウント後の要素が代入されます。しかし、参照の配列では、元の配列と同じ順序が保証されません

これはどういうことでしょうか?

実際のコードで見てみましょう。

<script setup>
import { ref, onMounted } from "vue";

const list = ref([1, 2, 3, 4, 5]);

const itemRefs = ref([]);

onMounted(() => {
  console.log(itemRefs.value.map((item) => item.textContent));
});
</script>

<template>
  <ul>
    <li v-for="item in list" ref="itemRefs">
      {{ item }}
    </li>
  </ul>
</template>

このコードでは、v-forディレクティブを使用して、配列list内の各要素を繰り返し表示しています。refを使用しているので、各li要素の値がitemRefsに参照されます。

ここが面倒な罠なのですがv-forは配列listを参照してループしているのでlistの順番でitemRefsに格納されていくはずですよね?しかし、v-for内でrefを使用すると参照の配列の順序は元の配列を保証しないのでitemRefsが[1,3,2,5,4]など元の配列と違う順番になることがあります。

では、この問題の解決方法を見ていきます。

配列の順序を保証する方法①

1つ目はHTMLのカスタムデータ属性を使う方法です。

カスタムデータ属性が分からない方はこちらを参考にして下さい。

この方法ではカスタムデータ属性としてdata-indexを新たに設定してdata-indexを昇順にすることでして参照配列の順番を保証しています。

<script setup>
import { ref, onMounted } from "vue";

const list = ref([
  { id: 1, value: 1 },
  { id: 2, value: 2 },
  { id: 3, value: 3 },
  { id: 4, value: 4 },
  { id: 5, value: 5 },
]);

const itemRefs = ref([]);

onMounted(() => {
  const sortedItems = itemRefs.value.sort(
    (a, b) => a.dataset.index - b.dataset.index
  );
  console.log(sortedItems.map((item) => item.textContent));
});
</script>

<template>
  <ul>
    <li
      v-for="item in list"
      ref="itemRefs"
      :key="item.id"
      :data-index="item.id"
    >
      {{ item.value }}
    </li>
  </ul>
</template>

配列の順序を保証する方法②

2つ目は関数を使って参照する方法です。

この方法では各リストアイテムの参照を取得するための関数setItemRefを定義しています。DOM要素が存在する場合にsetItemRef関数が各<li>要素の参照を受け取り、itemRefs配列に追加します。v-forディレクティブが生成する要素の順序で参照が配列に追加されるため、順序が保証されます。

<script setup>
import { ref, onMounted } from "vue";

const list = ref([
  { id: 1, value: 1 },
  { id: 2, value: 2 },
  { id: 3, value: 3 },
  { id: 4, value: 4 },
  { id: 5, value: 5 },
]);

const itemRefs = ref([]);

const setItemRef = (el) => {
  if (el) {
    itemRefs.value.push(el.textContent);
  }
};

onMounted(() => console.log(itemRefs.value));
</script>

<template>
  <ul>
    <li v-for="item in list" :ref="setItemRef" :key="item.id">
      {{ item.value }}
    </li>
  </ul>
</template>

まとめ

いかがでしたでしょうか?

今回はテンプレート参照の使い方についてまとめました。もし、v-for内でテンプレート参照を使うことがあればぜひ、この方法を使ってみてください。