はじめに
本連載では、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のサンプルで説明します。
親コンポーネント(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プロパティ)を渡してイベントを発行します。