TechVue.jsJapanese

[ Vue3 + TypeScript ] 子コンポーネント で実装した ドロップダウン で選択した内容を リアクティブ に 親コンポーネント と 連携する方法

Tech

リアクティブ な アプリケーション を 再利用可能で 独立した コンポーネント と言う概念を使ってが容易に実装できるのが Vue.js の利点の一つだといえます。 この コンポーネント の利点を活かすために 各 コンポーネント をあまり大きなサイズ にしないことが一つの ポイント になってきます。 一方で 単一の コンポーネント 内部で リアクティブ に実現できていた機能を コンポーネント に分割して実現しようとすると コンポーネント 間で データ のやりとりを実現する必要があります。 

本記事では 通常 ツリー 構造となる コンポーネント の 「親 コンポーネント から 子 コンポーネント」、 「子 コンポーネント から 親 コンポーネント」 のそれぞれについて リアクティブ な データ のやりとりを TypeScript で実現する方法について ドロップボックス (Select) を例に用いて 整理していきます。

macOS: 12.3
vue: 3.2.31
typescript: 4.5.5
vue3 API: Composition API
bootstrap: 5.1.3

Vue3 + TypeScript + Bootstrap5 プロジェクトの準備

[ads]

今回の記事で使用するプロジェクトを用意します。

  • npm int vue@3 を使ってプロジェクト作成
  • TypeScript を有効にする
  • bootstrap5 をインストール

参考記事:

親 コンポーネント (App.vue)

create-vue で作成された プロジェクト に存在する App.vue を 親 コンポーネント として利用します。 以下のように 子 コンポーネント として 新規に作成する Child.vue を インポート し それを template で呼び出す形にします。 style については、プロジェクト作成時から変更しないため、割愛しています。

<script setup lang="ts">
import Child from './components/Child.vue'
</script>
 
<template>
 <div>
   <Child />
 </div>
</template>

子コンポーネント (Child.vue)

続いて 子 コンポーネント を準備します。 src / components に 新規 ファイル として 以下の内容で Child.vue ファイル を作成します。

template には 、Bootstrap5 の公式サイト の サンプル を引用してきています。

<script setup lang="ts">
</script>
 
<template>
   <div>
       <select class="form-select" aria-label="Default select example">
           <option selected>Open this select menu</option>
           <option value="1">One</option>
           <option value="2">Two</option>
           <option value="3">Three</option>
       </select>
   </div>
</template>

その他

Bootstrap5 を使うために main.ts に以下の1行を追加します。

import { createApp } from 'vue'
import App from './App.vue'
import "bootstrap/dist/css/bootstrap.min.css"
 
createApp(App).mount('#app')

この段階でのアプリ動作

特に 各 コンポーネント に プロパティ もない状態で ドロップダウン が 動いている状態です。

次の ステップ では 親 コンポーネント である App.vue に設置した コンテンツ の内容に応じて、 子 コンポーネント に設置した ドロップボックス (Select) の値を リアクティブ に 変化させてみます。


親から子へ データを連携する (Props)

[ads]

子 コンポーネント に ドロップダウン と連動する プロパティ 追加

まずは 準備として Child.vue に ドロップダウン と連動する プロパティ を defineProps を用いて追加していきます。

<script setup lang="ts">
defineProps(['modelValue'])
</script>
 
<template>
   <div>
       <select
           class="form-select"
           aria-label="Default select example"
           v-model="modelValue">
           <option selected>Open this select menu</option>
           <option value="1">One</option>
           <option value="2">Two</option>
           <option value="3">Three</option>
       </select>
   </div>
</template>

ドロップダウン の選択に応じて値が変化する プロパティ modelValue を実装し、期待通り動作していることが分かります。

なお、この段階では modelValue という名称でなくても問題ありませんが、親 コンポーネント と連携する際には modelValue 以外の名称だとうまくいきませんので、注意してください。ここでは 親 コンポーネントとの連携も見据えて modelValue という名称で進めていきます。

参考:
https://vuejs.org/guide/components/events.html#v-model-arguments

親 コンポーネント に 子 コンポーネント と連動する変数 と その変数を変化させる テキストボックス を追加

親 コンポーネント の App.vue に 子 コンポーネント と連動する ref オブジェクト の 変数 item を追加します。ドロップダウンの初期値として “One” が表示されるように 値は 1 としています。

同時に、この itemv-model に指定した テキストボックス を追加します。ここで入力した値を 子 コンポーネント に連携させてみます。

<script setup lang="ts">
import { ref } from 'vue'
import Child from './components/Child.vue'
 
const item = ref('1')
</script>
 
<template>
 <div>
   Parent:
   <input v-model="item" />
   <hr />Child:
   <Child v-model="item" />
 </div>
</template>

早速 この コード を動かし、 テキストボックス に値を入力してみます。

親 コンポーネント に実装した テキストボックス に値を入力すると それに対応した 子 コンポーネント の ドロップダウン が変化していることが分かります。  親 コンポーネント の変数の値が Child コンポーネント での v-model で 子 コンポーネント 側へ リアクティブ に連携されているためです。

次の ステップ では、 ドロップボックス の選択に合わせて テキストボックスの内容を変化させてみます。 つまり 今の実装とは 逆で 「子 コンポーネント から 親 コンポーネント へ データ を連携」 するという流れになります。


子 コンポーネント から 親 コンポーネント へ データ を連携する (Emit)

[ads]

先ほどの状態で ドロップダウン から値を選択してみても、親 コンポーネント の テキストボックス の値は 変わりません。

先ほど、実装した処理は あくまで 親 から 子 への一方通行の連携だからです。 それでは 子 から 親 への データ 連携を実装してみます。 子 から 親 への データ 連携には Emit という機能を用います。

Emit の実装

まずは Emit 処理を defineEmits を用いて定義します。 ここでも defineProps での modelValue と同様に update:modelValue と言う名称で定義してください。 それ以外の名称ですと 親 に連携することができません。

<script setup lang="ts">
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

続いて 定義した Emit を呼び出す部分を実装します。 今回は ドロップダウン ですので select タグ に @change を追加することで ドロップダウン を選択する都度 Emit を呼び出し 親へ連携していこうと思います。

       <select
           class="form-select"
           aria-label="Default select example"
           v-model="modelValue"
           @change="$emit('update:modelValue', $event.target.value)"
       >

$event.target.valueemit の引数とすることで  option タグ の value で指定した値が親に連携されることになります。

それでは 早速この状態で実行していきます。

無事、 子 コンポーネント で実装した ドロップダウン で選択した項目に対応した値が 親 コンポーネント で実装した テキストボックス に反映されています。 先ほど実装した 親から子 への実装も引き続き有効ですので これで双方向のデータ 連携 が完成しました。

補足 Object is possibly ‘null’.ts(2531) への対応

ここまでの コード で目的の機能を実装することができましたが、 実は以下の TypeScript Warning が出力されています。

Object is possibly 'null'.ts(2531)

これは tsconfig.json で 以下のいずれかの設定をしている場合に出力されるためです。これらをいずれも false にすれば出力されなくなりますが、せっかくの警告が抑制されてしまうので、やはり根本解決してみることにします。

Type Annotations を明示する (HTMLSelectElement)

警告の原因は Type Annotations を省略しているため、 暗黙的に any 型 と判断されていることにあります。 ですので、 明示的に Type Annotations を記載することで 警告 に対応することができます。 TypeScript 公式 サイト でも 「any は型チェックできないため 基本的に避けるべき」 と説明されています。

select エレメント では HTMLSelectElement インターフェース を用いることになりますので この HTMLSelectElement を明示的にコードに記載していきます。

       <select
           class="form-select"
           aria-label="Default select example"
           v-model="modelValue"
           @change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
       >

このように記載することで 実装した機能の動作はそのままに TypeScript の 警告 が表示されなくなります。

参考:
https://vuejs.org/guide/typescript/composition-api.html#typing-event-handlers


まとめ

[ads]

親 -> 子 defineProps を用いる

  • modelValue という名称を使う

子 -> 親 defineEmits を用いる

  • update:modelValue という名称を使う

Object is possibly 'null' へ対応するためには Type Annotation を明示する (ここでは HTMLSelectElement


[ads]

今回使用した サンプル コード

App.vue

<script setup lang="ts">
import { ref } from 'vue'
import Child from './components/Child.vue'

const item = ref('1')
</script>
<template>
  <div>
    Parent:
    <input v-model="item" />
    <hr />Child:
    <Child v-model="item" />
  </div>
</template>

Child.vue

<script setup lang="ts">
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
    <div>
        <select
            class="form-select"
            aria-label="Default select example"
            v-model="modelValue"
            @change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
        >
            <option selected>Open this select menu</option>
            <option value="1">One</option>
            <option value="2">Two</option>
            <option value="3">Three</option>
        </select>
    </div>
</template>
[ads]
Ads