はじめに
本連載では、JavaScriptを利用して動的なWebページを構築できるフレームワークVue.jsを、データの型指定ができるように拡張されたAltJS(コンパイルしてJavaScriptにする言語)であるTypeScriptで活用する方法を、順を追って説明しています。
Vue.jsにおいて、Webページのある一部分を構成する実装単位を「コンポーネント」と呼びます。これまでの連載記事では、Webページ全体に対応するコンポーネント(App.vueファイル)を単体で利用してきましたが、より表示が複雑な実際の開発では、1つのWebページを複数のコンポーネントに分割して開発するのが一般的です。そこで本記事では、複数のコンポーネントを利用してWebページを作成する方法を説明します。
対象読者
- これからVue.jsに入門したい方
- 新しいトレンドを常に取り入れたい方
- 比較的複雑なWebページをVue.jsで作ってみたい方
必要な環境
本記事のサンプルコードは、以下の環境で動作を確認しています。
- Windows 10 64bit版
- Node.js v16.15.1 64bit版
- Vue.js 3.2.36
- TypeScript 4.7.2
- Microsoft Edge 102.0.1245.39
サンプルコードを実行するには、サンプルのフォルダーで「npm install」コマンドを実行してライブラリーをダウンロード後、「npm run dev」コマンドを実行して、Webブラウザーで「http://localhost:3000/」を表示します。
サンプルコードの*.vueファイルには、表示を調整するため<style>部が記述されている場合がありますが、記事中では省略します。詳細はサンプルコードを参照してください。
親から子に情報を伝達する、コンポーネントのプロパティ(props)
最初に、スマートフォンのリストが表示される図1のサンプルで、コンポーネントの利用法を説明していきます。Webページ全体に対応するコンポーネント(App.vueファイル)の配下に、リストの1行に対応する子コンポーネントを複数配置します。
親コンポーネント側の実装
他のコンポーネントを含める親となるApp.vueファイルはリスト1の通りです。
<template>
<div>
<!-- Phone1コンポーネントの描画 ...(1)-->
<Phone1 name="Galaxy S22" vendor="Samsung" :price="125030"></Phone1>
<Phone1 name="AQUOS sense6" vendor="SHARP" :price="57024"></Phone1>
<!-- Phone2コンポーネントの描画 ...(2)-->
<div v-for="phone in phoneDataArray" v-bind:id="phone.name">
<Phone2 :phoneData="phone"></Phone2>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
// 子コンポーネントをインポート ...(3)
import Phone1 from './components/Phone1.vue'
import Phone2 from './components/Phone2.vue'
// PhoneDataクラスをインポート ...(4)
import PhoneData from './PhoneData'
export default defineComponent({
// 使用するコンポーネントを指定 ...(5)
components: {
Phone1,
Phone2
},
// Composition APIでAppコンポーネントを初期設定
setup() {
const phoneDataArray = [ // phoneDataArrayの初期化 ...(6)
new PhoneData('Xperia 1 IV', 'Sony', 192930),
new PhoneData('arrows We', 'FCNT', 22000)
]
return { phoneDataArray }
}
})
</script>
(1)では<Phone1>、(2)では<Phone2>コンポーネントを利用して、スマートフォンの1行を描画します。このようにコンポーネントを使用するには、<script>部の(3)でコンポーネントの実装を別ファイルからインポートして、defineComponentメソッド内のcomponents(5)に設定します。
(1)のPhone1ではname(名前)、vendor(メーカー)、price(価格)を属性値としてコンポーネントに渡しています。「:price」は「v-bind:price」の省略で、priceに文字列型ではなく数字型でデータを渡すための記述です。
一方(2)では、phoneDataArray配列の各要素をv-forディレクティブで抽出して、Phone2コンポーネントのphoneData属性に与えています。phoneDataArray配列は<script>部の(6)で定義されており、(4)でインポートしたPhoneDataクラスオブジェクトの配列になっています。なお、PhoneDataクラスにはname、vendor、priceのプロパティとコンストラクター、カンマ区切りの価格を取得するdispPriceプロパティが実装されています。詳細はサンプルコードを参照してください。
子コンポーネント側の実装
次に、App.vueから参照される子コンポーネントの実装を見ていきます。まずPhone1コンポーネントはリスト2の通りです。
<template>
<div class="elem-body">
<div class="elem-name">{{ name }}</div>
<div>
【メーカー】{{ vendor }}
【価格】{{ Intl.NumberFormat('ja-JP').format(price) }}円 {{/*(1)*/}}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: { // プロパティ ...(2)
vendor: String, // 文字列型 ...(2a)
name: String, // 文字列型
price: { // より細かい指定を行う例 ...(2b)
type: Number, // 数字型
default: 0, // 初期値0
required: true // 必須
}
}
})
</script>
コンポーネントに指定するプロパティは、<script>部のdefineComponentメソッド内のprops(2)に指定します。各プロパティには(2a)や(2b)の記述で、型や初期値、必須かどうかを指定できます。ここでは文字列型のvendorとname、数字型(初期値0、必須)のpriceプロパティを定義しています。これらのプロパティを<template>部で参照してコンポーネントを表示します。なお(1)は、priceの値をカンマ区切りにする実装です。
コンポーネントのプロパティには文字列や数字だけではなく、特定の型のオブジェクトを渡すこともできます。Phone2コンポーネントの実装(リスト3)で説明します。
import { defineComponent } from "vue"
import PhoneData from "@/PhoneData" // PhoneDataクラスをインポート ...(1)
export default defineComponent({
props: {
phoneData: PhoneData // PhoneData型プロパティ ...(2)
}
})
(1)でPhoneDataクラスをインポートします。「@/PhoneData」は、プロジェクトのルートに存在するPhoneData.tsをインポートする意味です。defineComponentメソッドのpropsに(2)の通り記述して、phontDataプロパティの型をPhoneDataクラスに指定できます。
子から親にイベントを伝達するemit
以下で紹介するemitは、子コンポーネントで発生したイベントを親コンポーネントに伝える仕組みです。子コンポーネント内で「購入する」ボタンを押すと、子コンポーネントの内容に対応したダイアログ表示を行う図2のサンプルで説明します。
図2 子コンポーネントからイベントを発生させるサンプル(p002-emit)
親コンポーネント(App.vue)はリスト4の通りです。
<template>
<div>
<div v-for="phone in phoneDataArray" v-bind:id="phone.name">
<PhoneWithBuy :phoneData="phone"
@onBuy="onBuyFromComponent"><!-- onBuyイベントを設定 ...(1)-->
</PhoneWithBuy>
</div>
</div>
</template>
<script lang="ts">
(略)
export default defineComponent({
components: {
PhoneWithBuy
},
setup() {
const phoneDataArray = [
new PhoneData('Galaxy S22', 'Samsung', 125030),
(略)
]
// PhoneWithBuyコンポーネントのonBuyイベントハンドラー ...(2)
const onBuyFromComponent = ((phone: PhoneData) => {
alert(`${phone.name}(${phone.dispPrice}円)を購入します。`)
})
return {
phoneDataArray, onBuyFromComponent
}
}
})
</script>
(1)の記述で、コンポーネントのonBuyイベント発生時に(2)のonBuyFromComponentメソッドが実行されるようになります。このメソッドでは、引数に渡されるPhoneDataクラスの変数phoneから、nameとdispPriceプロパティを取得してダイアログ表示します。
onBuyイベントは子コンポーネント(PhoneWithBuy.vue)でリスト5の通り実装します。
export default defineComponent({
(略:propsの指定)
emits: [
'onBuy' // emitする可能性があるイベントを指定 ...(1)
],
setup(props, context) { // setupの引数でpropsとcontextを受け取る ...(2)
// 購入するボタンクリック時の処理
const onClickBuyButton = (() => {
context.emit('onBuy', props.phoneData) // onBuyイベントをemit ...(3)
})
return { onClickBuyButton }
}
})
まずdefineComponentで、コンポーネントが送信(emit)する可能性があるイベント名(ここではonBuy)を、(1)のemits配列に指定します。Composition APIのsetupメソッド(2)では、第1引数にプロパティに対応するprops変数、第2引数にemitメソッドを含むcontext変数が受け取れます。ボタン押下時に実行されるonClickBuyButtonメソッドでは、(3)のcontext.emitメソッドの第1引数にイベント名onBuy、第2引数に引数(ここではprops.phoneDataプロパティ)を渡してイベントを発行します。
