リアクティブ な アプリケーション を 再利用可能で 独立した コンポーネント と言う概念を使ってが容易に実装できるのが 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 プロジェクトの準備
今回の記事で使用するプロジェクトを用意します。
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)
子 コンポーネント に ドロップダウン と連動する プロパティ 追加
まずは 準備として 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 としています。
同時に、この item
を v-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)
先ほどの状態で ドロップダウン から値を選択してみても、親 コンポーネント の テキストボックス の値は 変わりません。
先ほど、実装した処理は あくまで 親 から 子 への一方通行の連携だからです。 それでは 子 から 親 への データ 連携を実装してみます。 子 から 親 への データ 連携には 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.value を emit
の引数とすることで option
タグ の value
で指定した値が親に連携されることになります。
それでは 早速この状態で実行していきます。
無事、 子 コンポーネント で実装した ドロップダウン で選択した項目に対応した値が 親 コンポーネント で実装した テキストボックス に反映されています。 先ほど実装した 親から子 への実装も引き続き有効ですので これで双方向のデータ 連携 が完成しました。
補足 Object is possibly ‘null’.ts(2531) への対応
ここまでの コード で目的の機能を実装することができましたが、 実は以下の TypeScript Warning が出力されています。
Object is possibly 'null'.ts(2531)
これは tsconfig.json
で 以下のいずれかの設定をしている場合に出力されるためです。これらをいずれも false
にすれば出力されなくなりますが、せっかくの警告が抑制されてしまうので、やはり根本解決してみることにします。
- “strict“: true
- “noImplicitAny“: true
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
まとめ
親 -> 子 defineProps
を用いる
modelValue
という名称を使う
子 -> 親 defineEmits
を用いる
update:modelValue
という名称を使う
Object is possibly 'null'
へ対応するためには Type Annotation を明示する (ここでは HTMLSelectElement
)
今回使用した サンプル コード
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>