🥦

Vue3で動的に増減する配列をテンプレート参照する

2024/12/13に公開

本記事は『Vue Advent Calendar 2024』の13日目の記事です。

https://qiita.com/advent-calendar/2024/vue

Vueではテンプレート内のDOM要素や子コンポーネントに直接アクセスする機会は少ないのですが、それでもたまに必要になります。
例えば、input要素にfocusを使って入力欄にフォーカスさせたり、子コンポーネントのメソッドを呼び出す場合です。

公式に書いている通り、ref という属性を使えば実現が可能です。この機能を「テンプレート参照」と呼びます。

https://ja.vuejs.org/guide/essentials/template-refs

とある開発シーンにて、 v-for でリストをループして表示している処理で、さらにリストの中身が動的に増減するようなケースでもテンプレート参照を使いたかったので、その方法をメモしておきます。

前提条件

  • Vue 3.5.12

今回の内容はVue 3.5のリリース内容に依存しているため、それより古いバージョンでは適用できませんのでご了承ください。

私たちのプロジェクトでは、Vueのコンポーネントを単一ファイルコンポーネント(SFC) + Composition API + TypeScriptで統一しているため、コード例はこのスタイルで説明しています。

1個の場合

親コンポーネントでボタンが押された時に、子コンポーネントのメソッドを呼び出す例です。

Parent.vue
<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>
Child.vue
<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 を使用すると、対応する参照には配列値が格納されます。

Parent.vue 配列版
<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>

今度は、配列の要素を追加できるボタンを配置してみます。

Parent.vue 配列+要素追加版
<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 引数として要素への参照を受け取ります:

実際のサンプルです。

Parent.vue 配列+要素追加+連想配列版
<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つの変数でペアとして管理したり、要素が動的に増減する場合でも連想配列のキーでアクセスできたり、柔軟な使い方ができそうですね。

今回紹介した内容は全てドキュメントに書かれている使い方なのですが、最初は流し読みしてしまっていました。
実際に使いたいユースケースが出てきた時に色々と調べてドキュメントに戻ってきたので、同じようなところで困っている方の参考になれば幸いです。

参考

https://medium.com/@guillaume.bretou/handling-dynamic-component-refs-in-vue-3-templates-d8a403432b3e

株式会社キャリオット

Discussion