Vue3で動的に増減する配列をテンプレート参照する
本記事は『Vue Advent Calendar 2024』の13日目の記事です。
Vueではテンプレート内のDOM要素や子コンポーネントに直接アクセスする機会は少ないのですが、それでもたまに必要になります。
例えば、input要素にfocusを使って入力欄にフォーカスさせたり、子コンポーネントのメソッドを呼び出す場合です。
公式に書いている通り、ref
という属性を使えば実現が可能です。この機能を「テンプレート参照」と呼びます。
とある開発シーンにて、 v-for
でリストをループして表示している処理で、さらにリストの中身が動的に増減するようなケースでもテンプレート参照を使いたかったので、その方法をメモしておきます。
前提条件
- Vue 3.5.12
今回の内容はVue 3.5のリリース内容に依存しているため、それより古いバージョンでは適用できませんのでご了承ください。
私たちのプロジェクトでは、Vueのコンポーネントを単一ファイルコンポーネント(SFC) + Composition API + TypeScriptで統一しているため、コード例はこのスタイルで説明しています。
1個の場合
親コンポーネントでボタンが押された時に、子コンポーネントのメソッドを呼び出す例です。
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import Child from './Child.vue';
const child = useTemplateRef('child');
const onclick = () => {
child.value?.sayHello('World');
};
</script>
<template>
<Child ref="child" />
<button @click="onclick">Click Me!</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const message = ref<string>('');
// 親から呼ばれるメソッド
const sayHello = (name: string) => {
message.value = 'Hello, ' + name;
};
defineExpose({
sayHello
});
</script>
<template>
<p>
{{ message }}
</p>
</template>
useTemplateRef
はVue3.5で導入された方法です。以前は以下のように書く必要がありました。 この時、ref
属性の値と同じ名前で変数を宣言する必要があります。
const child = ref<InstanceType<typeof Child>>();
複数の場合(配列で参照を管理するパターン)
v-for
の中で ref
を使用すると、対応する参照には配列値が格納されます。
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import Child from './Child.vue';
const child = useTemplateRef('child');
const onclick = () => {
child.value?.at(0)?.sayHello('Alice');
child.value?.at(1)?.sayHello('Bob');
child.value?.at(2)?.sayHello('Charlie');
};
</script>
<template>
<template v-for="name in Array(3).fill(0)">
<Child ref="child" />
</template>
<button @click="onclick">Click Me!</button>
</template>
今度は、配列の要素を追加できるボタンを配置してみます。
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import Child from './Child.vue';
const names = ref<string[]>(['Alice', 'Bob', 'Charlie']);
const child = useTemplateRef('child');
const add = () => {
// ランダムな位置に追加
const index = Math.floor(Math.random() * names.value.length);
names.value.splice(index, 0, 'Dave');
};
const onclick = () => {
for (let i = 0; i < names.value.length; i++) {
child.value?.at(i)?.sayHello(names.value[i]);
}
};
</script>
<template>
<!-- 確認用 -->
<p>Names: {{ names }}</p>
<template v-for="name in names">
<Child ref="child" />
</template>
<!-- 追加ボタン -->
<button @click="add">Add new member</button>
<button @click="onclick">Click Me!</button>
</template>
こちらも問題なく動作します。
複数の場合(連想配列で参照を管理するパターン)
元データの配列が増減する場合など、インデックスの扱いで大変になってきた場合には、連想配列でデータを管理したくなります。その場合、文字列のキーではなく関数にバインドする方法が有効です。
以下はドキュメントからの引用です。
ref 属性は、文字列のキーの代わりに、関数にバインドすることもできます。関数はコンポーネントが更新されるたびに呼び出され、要素の参照をどこに保持するかを柔軟に決めることができます。関数は、第 1 引数として要素への参照を受け取ります:
実際のサンプルです。
<script setup lang="ts">
import { ref } from 'vue';
import Child from './Child.vue';
// [名前: 年齢] の連想配列
const members = ref<{ [name: string]: number }>({ Alice: 22, Bob: 18, Charlie: 20 });
// [名前: テンプレート参照] の連想配列
const child = ref<{ [name: string]: InstanceType<typeof Child> }>({});
const add = () => {
// 追加
members.value['Dave'] = 30;
};
const onclick = () => {
const name = 'Alice';
child.value![name].sayHello(`${name} (age: ${members.value[name]})`);
};
</script>
<template>
<!-- 確認用 -->
<p>Names: {{ members }}</p>
<template v-for="name in Object.keys(members)">
<!-- 関数を使って要素の参照を保持する -->
<Child :ref="(el) => (child[name] = el as InstanceType<typeof Child>)" />
</template>
<button @click="add">Add new member</button>
<button @click="onclick">Click Me!</button>
</template>
関数を使って参照をセットすることで、データとテンプレート参照を1つの変数でペアとして管理したり、要素が動的に増減する場合でも連想配列のキーでアクセスできたり、柔軟な使い方ができそうですね。
今回紹介した内容は全てドキュメントに書かれている使い方なのですが、最初は流し読みしてしまっていました。
実際に使いたいユースケースが出てきた時に色々と調べてドキュメントに戻ってきたので、同じようなところで困っている方の参考になれば幸いです。
参考
Discussion