6.1 子コンポーネントの利用
6.1.1 コンポーネントとは
コンポーネントの基礎(App.vue)
コンポーネント1個
ひとつのコンポーネント(OneSection.vue)
コンポーネントとは、…
コンポーネントが複数
ひとつのコンポーネント(OneSection.vue)
コンポーネントとは、…
ひとつのコンポーネント(OneSection.vue)
コンポーネントとは、…
ひとつのコンポーネント(OneSection.vue)
コンポーネントとは、…
繰り返しに登場する部分
ひとつのコンポーネント(OneSection.vue)
コンポーネントとは、…
Web 画面にはこのように繰り返し登場する部分が多いので、これらを1個の部品として作成して何度も再利用できれば実装の手間が省ける。
さらに、HTML + CSS という見た目の部分だけでなく、そこに動作(スクリプト)部分が加わった部品であればいっそう再利用の価値が高まる。
このHTML、CSS、スクリプトをワンセットとして再利用可能な部品としたものがコンポーネントである。
VueではこのようなHTML + CSS + スクリプトのセットを .vue 拡張子を持つひとつのファイルにまとめて記述できる。これをシングルファイルコンポーネント(SFC)という。
App.vue もシングルファイルコンポーネントのひとつであり、Vue プロジェクトではこの App.vue が全ての画面表示(処理)の始点となる。
6.1.2 コンポーネントの作り方
App.vue から外面の部品となる別のコンポーネントを読み込んで利用するには、利用する先のコンポーネント(子コンポーネント)を作成する必要がある。
Vue プロジェクトではひとつのコンポーネントがひとつの .vue ファイルとなる。
App.vue 以外の SFC (.vueファイル)は src/components フォルダに作成することになっていて、 .vue ファイルを作成すればそれだけでそれがひとつのコンポーネントとなる。
サンプルで App.vue から利用するコンポーネントを src/components/OneSection.vue とする。
表示だけなのでスプリットブロックは無い。
<template>
<section class="box">
<h4>ひとつのコンポーネント</h4>
<p>コンポーネントとは、・・・</p>
</section>
</template>
<style>
.box {
border: green 1px dashed;
margin: 10px;
}
</style>
6.1.3 子コンポーネントの利用方法
<script setup lang="ts">
import OneSection from './components/OneSection.vue' // 1.
</script>
<template>
<h1>コンポーネント基礎</h1>
<section>
<h2>コンポーネント1個</h2>
<OneSection /> // 2.
</section>
<section>
<h2>コンポーネントが複数</h2>
<OneSection /> // 2.
<OneSection /> // 2.
<OneSection /> // 2.
</section>
</template>
<style>
section {
border: blue 1px solid;
margin: 10px;
}
</style>
子コンポーネントを利用するときの手順
- 子コンポーネントの .vue ファイルをインポートする
.js ファイルや .ts ファイルのモジュールをインポートする場合は拡張子を記述しないことになっているが .vue ファイルの場合は拡張子が必要な点に注意 - テンプレートブロックにタグを記述
インポートしたときのインポート名と同じ名称のタグを記述すると、そのタグの位置に該当コンポーネントの内容が自動的にレンダリングされる
6.2 コンポーネントの独立性と CSS の扱い
6.2.1 処理が含まれたコンポーネントを埋め込む
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('名無し') // 1-1
</script>
<template>
<section>
<p>{{ name }}さんですね!</p> // 1-2
<input type="text" v-model="name" /> // 1-3
</section>
</template>
<style scoped> /* 1-4 */
section {
border: green 1px dashed; /* 1-5 */
margin: 10px;
}
</style>
<script setup lang="ts">
import WithMode from './components/WithModel.vue'
</script>
<template>
<h1>コンポーネントの独立性</h1>
<section>
<h2>v-modelを含むコンポーネント</h2>
<WithMode /> <!-- 2-1 -->
<WithMode /> <!-- 2-1 -->
</section>
</template>
<style>
section {
border: blue 1px solid; /* 2-2 */
margin: 10px;
}
</style>
コンポーネントが2つ表示される。
入力欄を変更すると表示が変わるのはそれぞれの子コンポーネントの内部のみである。
これがコンポーネントの独立性である。
6.2.2 コンポーネント内の処理はコンポーネント内で完結
WithModel.vue
(1-2)のマッシュ構文と(1-3)の入力コントロールが v-model ディレクティブと(1-1)のリアクティブ変数 name を介して連動している。
そのため入力欄の内容を変更するとそれに合わせて(1-2)の p タグ内のマスタッシュ構文の表示が変化する。
子コンポーネントである WithModel.vue の中で見た場合、特に新しいことはない。
App.vue
App.vue では WithModel コンポーネントを2個埋め込んでいる。
結果としてはひとつの画面に表示されるが、処理としてはひとつひとつの WithModel コンポーネント内で完結した別の処理として動作する。
6.2.3 スタイルブロックを独立させる scoped 属性
コンポーネントは独立しているが、それは処理に関してのみで、スタイルブロックに記述した CSS に関してはコンポーネントごとに独立しておらず、他のコンポーネントのスタイルブロックの影響を受ける。
これをコンポーネントごとに独立させるための記述が、WithModel.vue の(1-4)の style タグの scoped 属性である。
App.vue の「v-modelを含むコンポーネント」と表示された section タグの枠線は実線であり、これに該当するスタイル記述は(2-2)である。
WithModelコンポーネントの section タグの枠線は破線であり、これに該当するスタイル記述は(1-5)である。
この状態で WithModel (1-4)の scoped 属性を削除すると、全ての枠線が破線になる。
これは、App.vue の section タグセレクタと WithModel.vue の section タグセレクタの両方が読み込まれ、後から読み込まれた WithModel.vue の section タグセレクタが採用されたためである。
この現象を避けるために使われるのが style タグに記述した scoped 属性であり、この仕組みを Scoped CSS という。
6.2.4 Scoped CSS のカラクリ
<div id="app" data-v-app="">
<h1>コンポーネントの独立性</h1>
<section>
<h2>v-modelを含むコンポーネント</h2>
<section data-v-eee87bea="">
<p data-v-eee87bea="">名無しさんですね!</p>
<input data-v-eee87bea="" type="text">
</section>
<section data-v-eee87bea="">
<p data-v-eee87bea="">名無しさんですね!</p>
<input data-v-eee87bea="" type="text">
</section>
</section>
</div>
レンダリング結果を見ると WithModel.vue 由来のタグには全て「data-v-eee87bea」という属性が記述されている。
section タグのスタイルを確認すると下記のようになっている。
section[data-v-eee878bea] {
border: green 1px dashed;
margin: 10px;
}
セレクタが単なる section タグセレクタではなく「section[data-v-eee878bea]」という属性セレクタになっており、絞り込み対象がレンダリング結果で WithModel.vue 由来のタグに付与されていた属性と一致する。
Scoped CSS が設定されたコンポーネント由来のタグには全て「data-v-####」という属性が自動で付与され、スタイルブロックに記述したセレクタには同じ属性セレクタが自動で付与される仕組みになっている。
プロジェクト全体のCSS設計
- グローバルに適用したい CSS 記述は App.vue のスタイルブロックに記述する。
- App.vue 以外のコンポーネントのスタイルブロックには原則 scoped 属性を記述して Scoped CSS とする。
6.3 親から子へのコンポーネント間通信
6.3.1 親からデータをもらう Props
親コンポーネントから子コンポーネントへの通信、親コンポーネントからのデータを子コンポーネントで受け取る仕組みを Props(プロップス)という。
<script setup lang="ts">
interface Props { // (1)
title: string
content: string
}
defineProps<Props>() // (2)
</script>
<template>
<section class="box">
<h4>{{ title }}</h4>
<p>{{ content }}</p>
</section>
</template>
<style scoped>
.box {
border: green 1px dashed;
margin: 10px;
}
</style>
<script setup lang="ts">
import OneInfo from './components/OneInfo.vue'
</script>
<template>
<h1>Props基礎</h1>
<section>
<h2>属性に直接記述</h2>
<OneInfo title="Propsの利用" content="子コンポーネントにデータを渡すにはPropsを利用する。" />
</section>
</template>
<style>
section {
border: blue 1px solid;
margin: 10px;
}
</style>
OneInfo.vue
(1)個々のProp を記述したインターフェースを定義する
interface Props {
title: string
content: string
}
インターフェース名を Props としているが、この名前は何でもかまわない。
ただし、特段の理由がない限り Props としておくのが良い。
親コンポーネントから受け取りたいデータ(各 Prop)は Props インターフェースのメンバとして定義する。
(2)defineProps() 関数を実行する
インターフェースを Props として利用するためには defineProps() 関数を実行する。
その際 <> 内に該当のインタフェースを記述したジェネリクス型指定(動的型指定)を行う。
これによって指定したインタフェースが Props として機能するようになる。
Props 定義の構文
interface Props {
各Prop名: データ型
:
}
defineProps<Props>()
6.3.2 親から Props にデータを渡す方法
子コンポーネントを表示するタグに 子コンポーネントの個々の Prop 名を属性として記述することによって属性値が子コンポーネントの該当する Prop 名のテンプレート変数の値として格納される。
<OneInfo title="Propsの利用" content="子コンポーネントにデータを渡すにはPropsを利用する。" />
Props へのデータ渡しの構文
<子コンポーネント名
各Prop名="このPropsに渡す値"
:
/>
6.3.3 親のテンプレート変数を Props に渡す方法
<script setup lang="ts">
interface Props {
title: string
content: number
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h4>{{ title }}</h4>
<p>{{ content }}</p>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import OneInfo from './components/OneInfo.vue'
const propsTitle = ref('発生した乱数')
const rand = Math.round(Math.random() * 100)
const propsContent = ref(rand)
</script>
<template>
<h1>Props基礎</h1>
<section>
<h2>テンプレート変数を利用</h2>
<OneInfo v-bind:title="propsTitle" v-bind:content="propsContent" />
</section>
</template>
<style>
section {
border: blue 1px solid;
margin: 10px;
}
</style>
スクリプトブロックで用意したテンプレート変数を割り当てる場合は v-bind ディレクティブを利用する。
v-bind の引数部分(コロンの次の記述)が Porp 名そのものとなる。
こうすることでその Prop にテンプレート変数の値が格納される。
しかも、テンプレート変数のリアクティブシステムがそのまま働くので親コンポーネント内でテンプレート変数の値を変更すると子コンポーネント内に表示された Props の値も連動して変化する。
テンプレート変数の Props へのデータ渡し構文
<子コンポーネント名
v-bind:Prop名="テンプレート変数名"
:
/>
6.3.4 v-for と Props との組み合わせ
<script setup lang="ts">
interface Props {
title: string
content: string
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h4>{{ title }}</h4>
<p>{{ content }}</p>
</section>
</template>
<style scoped>
.box {
border: green 1px dashed;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneInfo from './components/OneInfo.vue'
const weatherListInit = new Map<number, Weather>()
weatherListInit.set(1, { id: 1, title: '今日の天気', content: '今日は一日中、晴れでしょう。' })
weatherListInit.set(2, { id: 2, title: '明日の天気', content: '明日は一日中、雨れでしょう。' })
weatherListInit.set(3, { id: 3, title: '明後日の天気', content: '明後日は一日中、雪でしょう。' })
const weatherList = ref(weatherListInit)
interface Weather {
id: number
title: string
content: string
}
</script>
<template>
<h1>Props基礎</h1>
<section>
<h2>ループでコンポーネントを生成</h2>
<OneInfo
v-for="[id, weather] in weatherList"
v-bind:key="id"
v-bind:title="weather.title"
v-bind:content="weather.content"
/>
</section>
</template>
ループするタグは通常の HTML タグや template タグではなく、子コンポーネント OneInfoである。
ループ対象のタグ内ではエイリアスに記述した変数がそのまま利用できる。
そこで、idを v-bind:key に指定するのに加え Pros にデータを渡す v-bind の値にループで取り出した各要素を指定することでこれらを子コンポーネントに渡すことができる。
この結果、リストデータの要素の数だけそれぞれのデータに基づいた子コンポーネントがレンダリングされる。
6.4 Props の応用
親コンポーネントから渡された Props のデータはそのまま表示するだけではなくスクリプトブロックで利用することができる。
<script setup lang="ts">
import { ref, computed } from 'vue'
// Propsインターフェースの定義
interface Props { // (1)
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
// Propsオブジェクトの設定
const props = defineProps<Props>() // (2)
// このコンポーネント内で利用するポイント数のテンプレート変数
const localPoints = ref(props.points) // (3)
// Propsのnoteを加工する算出プロパティ
const localNote = computed((): string => { // (4)
let localNote = props.note // (5)
if (localNote == undefined) {
localNote = '--'
}
return localNote
})
// [ポイント加算]ボタンをクリックしたときのメソッド
const pointUp = (): void => { // (6)
localPoints.value++
}
</script>
<template>
<section class="box">
<h4>{{ name }}さんの情報</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>メールアドレス</dt>
<dd>{{ email }}</dd>
<dt>保有ポイント</dt>
<dd>{{ localPoints }}</dd>
<dt>備考</dt>
<dd>{{ localNote }}</dd>
</dl>
<button v-on:click="pointUp">ポイント加算</button>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'
// 会員リストデータを用意
const memberListInit = new Map<number, Member>() // (1)
memberListInit.set(33456, {
id: 33456,
name: '田中太郎',
email: 'bou@example.com',
points: 35,
note: '初回入会特典あり',
})
memberListInit.set(47783, {
id: 47783,
name: '鈴木二郎',
email: 'mue@example.com',
points: 53,
})
const memberList = ref(memberListInit)
// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => { // (2)
let total = 0
for (const member of memberList.value.values()) {
total += member.points
}
return total
})
// 会員情報インターフェース
interface Member { // (3)
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>
<template>
<section>
<h1>会員リスト</h1>
<p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
<!-- OneMemberをv-forでループ-->
<OneMember <!-- (4) -->
v-for="[id, member] in memberList"
v-bind:key="id"
v-bind:id="id"
v-bind:name="member.name"
v-bind:email="member.email"
v-bind:points="member.points"
v-bind:note="member.note"
/>
</section>
</template>
各会員情報ボックス内の[ポイント加算]ボタンをクリックすると会員保有ポイントが増加する。
一方で、全会員の保有ポイントの合計は変化しない。
App.vue
- (1)会員リストデータを用意。
インターフェースは(3)で定義。 - (2)会員リスト内の全会員のポイント合計を求める算出プロパティ。
- (3)インターフェース。
- (4)リストデータを元に子コンポーネント OneMember を v-for でループして表示。
インターフェース(3)で定義したデータの各 Prop データを渡している。
子コンポーネント OneMember には同様の Props 定義がある。
OneMember.vue
- (1)インターフェース Props。
App.vue の Member インターフェースと同じ内容。 - (2)defineProps() 関数のジェネリクス型指定として Props インターフェースを渡している。
関数の戻り値を変数 props として受け取っている。
defineProps() 関数の戻り値
非必須項目の note はデータがある場合はそのまま表示し、無い場合は「–」と表示している。
その為にはスクリプトブロックで Props の値の有無によって処理を変えるコードが必要である。
このような場合は、defineProps() 関数の戻り値を変数(この例では props変数)で受け取る。
こうすることで、このスクリプトブロック内では Props のプロパティとして個々の Prop データが取り出せるようになる。
const props = defineProps<Props>() // (2)
:
const localNote = computed((): string => { // (4)
let localNote = props.note // (5)
if (localNote == undefined) {
localNote = '--'
}
return localNote
})
6.4.2 Props の値の利用の注意点
Props の値は子コンポーネントでは直接変更できない。
Props の値はリアクティブシステムの管理対象であり親コンポーネントで値を変更するとそれが反映されるため、子コンポーネントで独自に値を変更すると不整合が起こりかねないので readonly とされている。
子コンポーネントで値を変更する可能性がある Props はいったんコンポーネント内で別のリアクティブ変数に値をコピーして利用する。
const localPoints = ref(props.points)
:
// [ポイント加算]ボタンをクリックしたときのメソッド
const pointUp = (): void => {
localPoints.value++
}
こうすることで localPoints は子コンポーネントである OneMember 独自のリアクティブ変数となり OneMember 内で自由に変更できるようになる。
この仕組みのおかげで [ポイント加算] ボタンをクリックすると子コンポーネント内で独立して各会員のポイント加算が行われる。
一方で、全会員の保有ポイントの合計値は親コンポーネントである App の管理課にある為、変更されない。
コンポーネント内のデータはコンポーネント内で閉じており、親コンポーネント管理下のデータであるポイント合計は子コンポーネントからは直接変更できないようになっている。
そのためこの値を変更するには子コンポーネント OneMember から親コンポーネント App に対してデータ変更の依頼「子から親へのコンポーネント間通信」を行う必要がある。
6.4.3 Props のデフォルト値
非必須項目の note のデータがない場合、算出プロパティを利用して「–」を表示していので、この「–」は非必須 Props のデフォルト値ということになる。
Props には算出プロパティを使わなくても、このようなデフォルト値を設定する仕組みがある。
<script setup lang="ts">
:
// Propsオブジェクトの設定
const props = defineProps<Props>()
// Propsのnoteを加工する算出プロパティ
const localNote = computed((): string => { // (4)
let localNote = props.note // (5)
if (localNote == undefined) {
localNote = '--'
}
return localNote
})
:
</script>
<template>
:
<dd>{{ localNote }}</dd>
:
</template>
<script setup lang="ts">
:
// Propsオブジェクトの設定
const props = withDefaults(
defineProps<Props>(),
{note: "--"}
)
:
</script>
<template>
:
<dd>{{ note }}</dd>
:
</template>
Props にデフォルト値を設定するには withDefaults() 関数を利用する。
note に対して「–」を指定しているので note のデータが存在しないときは「–」と表示される。
ただし、より複雑な条件分岐による表示の加工が必要な場合などはデフォルト値を使うこの方法では対応が難しくやはり算出プロパティを利用することいになる。
withDefaults(
defineProps<Props>(),
{
非必須Prop名: デフォルト値,
:
}
)
6.5 子から親へのコンポーネント通信
子から親へのコンポーネント間通信は Emit(エミット)という仕組みで実現できる。
6.5.1 子から親への通信はイベント処理
<script setup lang="ts">
interface Props {
rand: number
}
interface Emits {
(event: 'createNewRand'): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const onNewRandButtonClick = (): void => {
emit('createNewRand')
}
</script>
<template>
<section class="box">
<p>子コンポーネントで乱数を表示: {{ rand }}</p>
<button v-on:click="onNewRandButtonClick">新たな乱数を発生</button>
</section>
</template>
<style scoped>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'
const randInit = Math.round(Math.random() * 10)
const rand = ref(randInit)
const onCreateNewRand = (): void => {
rand.value = Math.round(Math.random() * 10)
}
</script>
<template>
<section>
<p>親コンポーネントで乱数を表示: {{ rand }}</p>
<OneSection v-bind:rand="rand" v-on:createNewRand="onCreateNewRand" />
</section>
</template>
子コンポーネントの [新たな乱数を発生] ボタンをクリックすると、表示された乱数の値が親コンポネント側、子コンポーネント側で同じように変化する。
子コンポーネントで emit() が実行されると、その引数の文字列(createNewRand)を使って次のような処理が行われる
- 子コンポーネントのタグ内の emit() の引数(createNewRand)に合致する親コンポーネント側の v-on ディレクティブの引数を探す。
- 合致した v-on ディレクティブの属性(onCreateNewRand)に注目する。
- 親コンポーネントのスクリプトブロックで、2.と合致するメソッドを実行する。
以上より emit() を実行されたタイミングで親コンポーネントのメソッドが実行されるようになる。
以上の処理の流れを踏まえてコードの記述手順をまとめると次のようになる。
- 親コンポーネントの記述
1-1.処理メソッドの用意
1-2.v-on ディレクティブの記述 - 子コンポーネントの記述
2-1.Emit 定義
2-2.Emitの実行
1-1.親コンポーネントの記述 ー 処理メソッドの用意
親コンポーネントに、子コンポーネントから通知を受けた際に実行する処理メソッドを用意する。
const onCreateNewRand = (): void => {
rand.value = Math.round(Math.random() * 10)
}
1-2.親コンポーネントの記述 ー v-on ディレクティブの記述
親コンポーネントの子コンポーネントタグに v-on ディレクティブを記述し、引数としてイベント名を表す任意の文字列、属性値として 1-1.のメソッド名を記述する。
<OneSection v-bind:rand="rand" v-on:createNewRand="onCreateNewRand" />
2-1.子コンポーネントの記述 ー Emit の定義
子コンポーネント内で Emit を定義する。
:
interface Emits { // 1.
(event: 'createNewRand'): void // 2.
}
:
:
const emit = defineEmits<Emits>() // 3.
:
- Emit をインターフェースで定義。
この例ではインターフェース名を Emits にしている。 - シグネチャ
シグネチャは Props のようなプロパティシグネチャではなくコールシグネチャとなり、引数定義として ( ) の中に引数名を記述する。
この引数名は何でも良いがイベントを表すため event あるいは e 記述することが多い。
この引数 event の型として、1-2.の v-on ディレクティブの引数として指定したイベント名(この例では createNewRand)を文字列で記述する。
戻り値は Void である。 - defineEmits() 関数を実行
定義した Emits インターフェースをジェネリクスとして型指定しながら defineEmits() 関数を実行する。
この仕組みは Props と同じだが Emit の場合は必ずその戻り値を変数 emit で受け取る。
interface Emits {
(event: "イベント名"): void
:
}
const emit = defineEmits<Emits>()
2-2.子コンポーネントの記述 ー Emit の実行
子コンポーネントで親側のメソッドを実行したいタイミングで emit() を実行する。
const onNewRandButtonClick = (): void => {
emit('createNewRand')
}
ここで実行する emit() は defineEmits() の戻り値である。
戻り値を関数として実行することで、その引数に該当するイベント名と関連付けられた親コンポーネントのメソッドが実行される仕組みとなっている。
この例ではメソッド onNewRandButtonClick の内部で emit() を実行しているので、[新たな乱数を発生] ボタンをクリックした時にこの処理が実行される。
以上のような手順で子コンポーネントから親コンポーネントのメソッドを実行した場合、そのメソッド内でリアクティブな変数の値を変更すると、これまでと同じようにそれをテンプレート変数として利用している部分の表示も変更される。
この例では、App.vue のテンプレート変数 rand がこれに該当する。
さらに、その変数を Props として子コンポーネントに渡している場合、子コンポーネントでもリアクティブシステムのおかげで値の変更に連動して表示が変更される。
6.5.2 親コンポーネントにデータを渡す方法
メソッドの実行とともにデータを受け渡したい場合は emit() の第2引数にそのデータを指定する。
<script setup lang="ts">
// Propsインターフェースの定義
interface Props {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
// Emit インターフェースの定義
interface Emits {
(event: 'incrementPoint', id: number): void // 1.
}
// Propsオブジェクトの設定
const props = withDefaults(
defineProps<Props>(),
{note: "--"}
)
// Emit の設定
const emit = defineEmits<Emits>()
// [ポイント加算]ボタンをクリックしたときのメソッド
const pointUp = (): void => {
emit('incrementPoint', props.id) // 2.
}
</script>
<template>
<section class="box">
<h4>{{ name }}さんの情報</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>メールアドレス</dt>
<dd>{{ email }}</dd>
<dt>保有ポイント</dt>
<dd>{{ points }}</dd> // 3.
<dt>備考</dt>
<dd>{{ note }}</dd>
</dl>
<button v-on:click="pointUp">ポイント加算</button>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'
// 会員リストデータを用意
const memberListInit = new Map<number, Member>()
memberListInit.set(33456, {
id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',})
memberListInit.set(47783, {
id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,})
const memberList = ref(memberListInit)
// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => {
let total = 0
for (const member of memberList.value.values()) {
total += member.points
}
return total
})
// Emit により実行されるメソッド
const onIncrementPoint = (id: number): void => { // 1.
// 処理関数の id に該当する会員情報オブジェクトを取得
const member = memberList.value.get(id)
// 会員情報オブジェクトが存在するならポイントインクリメント // 2.
if (member != undefined) {
member.points++
}
}
// 会員情報インターフェース
interface Member {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>
<template>
<section>
<h1>会員リスト</h1>
<p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
<!-- OneMemberをv-forでループ-->
<OneMember
v-for="[id, member] in memberList"
v-bind:key="id"
v-bind:id="id"
v-bind:name="member.name"
v-bind:email="member.email"
v-bind:points="member.points"
v-bind:note="member.note"
v-on:incrementPoint="onIncrementPoint" // 3.
/>
</section>
</template>
OnrMrmnrt.vue
- event 引数に加え第2引数として number 型の id を定義
- [ポイント加算]ボタンが押されたタイミングで emit() が実行され親コンポーネントの onIncrementPointメソッドが実行される
- リアクティブシステムにより子コンポーネントにも反映されているので Propsの points をそのまま表示するだけで済む
App.vue
- [ポイント加算]ボタンクリックのタイミングで emit() が実行され親コンポーネントの onIncremdntPoint メソッドが実行される。
onIncrementPoint は引数 id を受け取り、会員リストのMapオブジェクトから該当会員の会員情報オブジェクトを member として取得。 - 会員オブジェクトが存在するならポイントをインクリメント。
この member 内の points への変更はリアクティブシステムにより合計ポイントを表す算出プロパティ totalPoints だけでなく子コンポーネントにも反映される。 - 子コンポーネントタグに v-on ディレクティブを記述。イベント名を登録している。
6.5.3 v-model による子から親への通信
子コンポーネントから親コンポーネントへの通信は v-model を利用するとさらに簡単に実現できる。
<script setup lang="ts">
// Propsインターフェースの定義
interface Props {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
// Emit インターフェースの定義
interface Emits {
(event: 'incrementPoint', id: number): void
}
// Propsオブジェクトの設定
const props = withDefaults(
defineProps<Props>(),
{note: "--"}
}
// Emit の設定
const emit = defineEmits<Emits>()
// [ポイント加算]ボタンをクリックしたときのメソッド
const pointUp = (): void => {
emit('incrementPoint', props.id)
}
</script>
<template>
<section class="box">
<h4>{{ name }}さんの情報</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>メールアドレス</dt>
<dd>{{ email }}</dd>
<dt>保有ポイント</dt>
<dd>{{ points }}</dd>
<dt>備考</dt>
<dd>{{ note }}</dd>
</dl>
<button v-on:click="pointUp">ポイント加算</button>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
// Propsインターフェースの定義
interface Props {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
// Emit インターフェースの定義
interface Emits {
(event: 'update:points', points: number): void
}
// Propsオブジェクトの設定
const props = withDefaults(
defineProps<Props>(),
{note: "--"}
}
// Emit の設定
const emit = defineEmits<Emits>()
const onInput = (event: Event): void => {
const element = event.target as HTMLInputElement
const inputPoints = Number(element.value)
emit('update:points', inputPoints)
}
</script>
<template>
<section class="box">
<h4>{{ name }}さんの情報</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>メールアドレス</dt>
<dd>{{ email }}</dd>
<dt>保有ポイント</dt>
<dd>
<input
type="number"
v-bind:value="points"
v-on:input="onInput" />
</dd>
<dt>備考</dt>
<dd>{{ note }}</dd>
</dl>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'
// 会員リストデータを用意
const memberListInit = new Map<number, Member>()
memberListInit.set(33456, {id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',})
memberListInit.set(47783, {id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,})
const memberList = ref(memberListInit)
// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => {
let total = 0
for (const member of memberList.value.values()) {
total += member.points
}
return total
})
// Emit により実行されるメソッド
const onIncrementPoint = (id: number): void => { // 1.
// 処理関数の id に該当する会員情報オブジェクトを取得
const member = memberList.value.get(id)
// 会員情報オブジェクトが存在するならポイントインクリメント // 2.
if (member != undefined) {
member.points++
}
}
// 会員情報インターフェース
interface Member {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>
<template>
<section>
<h1>会員リスト</h1>
<p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
<!-- OneMemberをv-forでループ-->
<OneMember
v-for="[id, member] in memberList"
v-bind:key="id"
v-bind:id="id"
v-bind:name="member.name"
v-bind:email="member.email"
v-bind:points="member.points"
v-bind:note="member.note"
v-on:incrementPoint="onIncrementPoint" // 3.
/>
</section>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'
// 会員リストデータを用意
const memberListInit = new Map<number, Member>()
memberListInit.set(33456, {id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',})
memberListInit.set(47783, {id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,})
const memberList = ref(memberListInit)
// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => {
let total = 0
for (const member of memberList.value.values()) {
total += member.points
}
return total
})
// 会員情報インターフェース
interface Member {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>
<template>
<section>
<h1>会員リスト</h1>
<p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
<!-- OneMemberをv-forでループ-->
<OneMember
v-for="[id, member] in memberList"
v-bind:key="id"
v-bind:id="id"
v-bind:name="member.name"
v-bind:email="member.email"
v-model:points="member.points" // 1.
v-bind:note="member.note"
/>
</section>
</template>
v-modelの働き
これまでは子コンポーネントタグの属性として Props にデータを渡すディレクティブとして v-bind を使用していた。
これを v-model とするとそれだけで Emit による変更対象となる。
v-modelによってProps のデータを更新する際は以下の構文で記述する。
emit("update.Prop名", 値)
<script setup lang="ts">
// Propsインターフェースの定義
interface Props {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
// Emit インターフェースの定義
interface Emits {
(event: 'update:points', points: number): void // (1)
}
// Propsオブジェクトの設定
const props = withDefaults(
defineProps<Props>(),
{note: "--"}
}
// Emit の設定
const emit = defineEmits<Emits>() // (2)
const onInput = (event: Event): void => { // (3)
const element = event.target as HTMLInputElement // (4)
const inputPoints = Number(element.value)
emit('update:points', inputPoints) // (5)
}
</script>
<template>
<section class="box">
<h4>{{ name }}さんの情報</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>メールアドレス</dt>
<dd>{{ email }}</dd>
<dt>保有ポイント</dt>
<dd>
<input type="number" v-bind:value="points" v-on:input="onInput" /> // (6)
</dd>
<dt>備考</dt>
<dd>{{ note }}</dd>
</dl>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref, computed } from 'vue'
import OneMember from './components/OneMember.vue'
// 会員リストデータを用意
const memberListInit = new Map<number, Member>()
memberListInit.set(33456, {id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',})
memberListInit.set(47783, {id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,})
const memberList = ref(memberListInit)
// 会員リスト内の全会員のポイントの合計算出プロパティ
const totalPoints = computed((): number => {
let total = 0
for (const member of memberList.value.values()) {
total += member.points
}
return total
})
// 会員情報インターフェース
interface Member {
id: number
name: string
email: string
points: number
note?: string // ?はオプションパラメーター(引数省略可能)
}
</script>
<template>
<section>
<h1>会員リスト</h1>
<p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
<!-- OneMemberをv-forでループ-->
<OneMember
v-for="[id, member] in memberList"
v-bind:key="id"
v-bind:id="id"
v-bind:name="member.name"
v-bind:email="member.email"
v-model:points="member.points" // 1.
v-bind:note="member.note"
/>
</section>
</template>
子コンポーネントの準備
- インターフェース定義 (1)
- defineEmits実行 (2)
親コンポーネントの準備
- 子コンポーネントタグの属性としてPropsにデータを渡すディレクティブとしてv-modelにする
<oneMember
:
v-model:points="member.points"
:
/>
動作
- 子コンポーネントの(6)で数値入力コントロールで内容が変更
- inputイベントが発生
- (3)のonInputメソッドが実行される
- (4)で変更された値を取得
- (5)でemit()実行
- 第1引数は、親コンポーネントで v-model の対象を points にしているので update:points となる
- 第2引数は取得した入力値
- 以上により子コンポーネントに入力された値が親コンポーネントにイベントメソッドが記述されていなくても反映される
- リアクティブシステムにより合計算出プロパティである totalPoints にも変更が反映される
6.6 Provide と Inject
6.6.1 コンポーネント間通信のまとめと問題提起

親コンポーネントから子コンポーネントへデータを渡すには Props を利用する。
子コンポーネントから親コンポーネントへデータを渡すには Emit によるイベント処理を利用する。
これらの仕組みは二つ合わせて 「Props ダウン、イベントアップ」と呼ばれる。
しかしコンポーネント関係が複雑になると問題が発生する

Provide と Inject
Provide と Inject を利用すると、Props と Enit を複雑に組み合わせる必要がなくなる。
概念としては「App.vue にてアプリケーション全体で参照されるデータを提供(Provide)しておくと、配下のコンポーネントではどれだけ階層が深くなってもデータの注入(Inject)という方法が利用できる」という仕組みである。

6.6.2 サンプルプログラムの作成
会員リスト ← App.vue
会員の保有ポイントの合計: 88 ← BaseSection.vue
田中太郎さんの情報 ← OneSection.vue
ID
33456
メールアドレス
bow@example.com
保有ポイント
[35 ]
備考
初回入会特典あり。
鈴木二郎さんの情報 ← OneSection.vue
ID
47783
メールアドレス
bow@example.com
保有ポイント
[53 ]
備考
–
export interface Member {
id: number
name: string
email: string
points: number
note?: string
}
<script setup lang="ts">
import { reactive, provide } from 'vue'
import BaseSection from './components/BaseSection.vue'
import type { Member } from './interfaces'
// 会員リストの用意 // 1.
const memberList = new Map<number, Member>()
memberList.set(33456, {
id: 33456, name: '田中太郎', email: 'bou@example.com', points: 35, note: '初回入会特典あり',
})
memberList.set(47783, {
id: 47783, name: '鈴木二郎', email: 'mue@example.com', points: 53,
})
// 会員リストをProvide
provide('memberList', reactive(memberList)) // 2.
</script>
<template>
<BaseSection /> // 3.
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import OneMember from './OneMember.vue'
import type { Member } from '../interfaces'
// 会員情報リストをInject
const memberList = inject('memberList') as Map<number, Member> // 1.
// 保有ポイントの合計の算出プロパティ
const totalPoints = computed((): number => {
let total = 0
for (const member of memberList.values()) { // 2.
total += member.points
}
return total
})
</script>
<template>
<section>
<h1>会員リスト</h1>
<p>全会員の保有ポイントの合計: {{ totalPoints }}</p>
<OneMember v-for="id in memberList.keys()" <!-- 3. -->
v-bind:key="id"
v-bind:id="id" /> <!-- 4. -->
</section>
</template>
<style scoped>
section {
border: orange 1px dashed;
margin: 10px;
}
</style>
<script setup lang="ts">
import { computed, inject } from 'vue'
import type { Member } from '../interfaces' // 1.
// Propsインターフェースの提議
interface Props {
id: number
}
// Props オブジェクトの設定
const props = defineProps<Props>()
// 会員情報リストをInject
const memberList = inject('memberList') as Map<number, Member> // 2.
// 該当する会員情報の取得
const member = computed((): Member => { // 4.
return memberList.get(props.id) as Member // 3.
})
// Propsであるnoteを加工する算出プロパティ
const localNote = computed((): string => {
let localNote = member.value.note // 5.
if (localNote == undefined) {
localNote = '--'
}
return localNote
})
</script>
<template>
<section class="box">
<h4>{{ member.name }}さんの情報</h4>
<dl>
<dt>ID</dt>
<dd>{{ id }}</dd>
<dt>メールアドレス</dt>
<dd>{{ member.email }}</dd>
<dt>保有ポイント</dt>
<dd>
<input type="number" v-model.number="member.points" /> <!-- 6. -->
</dd>
<dt>備考</dt>
<dd>{{ localNote }}</dd>
</dl>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
6.6.3 Provide の利用方法
プロジェクトでのデータの関係

App コンポーネントのスクリプトブロックでは会員情報リストを生成しそれを Provide している。
以降の BasicSection コンポーネントと OneMember コンポーネントでは、ここで Provide された会員情報リストを Inject によって利用している。
App.vue
- 会員情報リストとして Map オブジェクトの memberList を生成。
- memberList を provide() 関数に渡すことで Provide が行われ他のコンポーネントで利用可能になる。
- BaseSection コンポーネントを読み込んで表示
Provide() 関数の構文
provide("Provide名", 値)
Provide名として、変数名と同じ memberList を指定している。
これによって他のコンポーネントでは memberList という名称で Inject して利用できる。
値としては、単純に memberList をわたすのではなく reactive() 関数を利用していて、これにより全コンポーネントにおいてリアクティブ変数として利用できるようになる。
通常 Provide を行う provide() 関数は reactive() 関数とセットで利用する。
リアクティブ変数を用意する関数にはもうひとつ ref() があるが、Provide されたデータをリアクティブする場合 ref() では使いずらい面がある。
ref() を利用したリアクティブ変数のデータにアクセスするには .value を利用する。
しかし、同じコンポーネント内ならば特に問題ないが Inject したデータに対して .value でアクセスしようとすると TypeScript では型エラーになる。
エラーを避けるため reactive() を利用する。
6.6.4 Inject の利用方法
BaseSection.vue
- Inject は inject() 関数を利用するだけで実現できる。
引数には Provide 名を渡す。
inject() の戻り値はそのままでは unknown 型となるため as キーワードによる型アサーション(型変換)を行ってもとのデータ型に復元しておく。 - Injectした membaerListのループ記述は、これまでは memberList.value.values() となっていたが今回のコードでは membaerList.values() となる。これは Provide の際 ref() ではなく reactive() を使用しているため。
- memberList をこのままテンプレート変数として使用し v-for でループしながら OneMember コンポーネントを表示している。
6.6.5 孫コンポーネントでも Inject は同様
これまでは OneMember コンポーネントをループする際、必要なデータを全て Props で渡していた。このサンプルでは OneMmember コンポーネント内でもデータを Inject して利用できるので、id のみを渡している。
v-for="[id, member] in memberList"
v-for="id in memberList.keys() v-bind:key="id" v-bind:id="id" />
OneMember.vue
- 外部モジュールで定義された型(interfaces.ts)をインポート。
- memberList を Injectして Mapオブジェクト memberListを取得。
- 取得したMapオブジェクト memberList に get() メソッドを利用して該当会員情報オブジェクト (Member オブジェクト)を取得。
- 取得した Member オブジェクトは OneMemberコンポーネント内ではリアクティブデータとはならないので、算出プロパティとしている。
算出プロパティとせずにmemberListからget()したMemberオブジェクトを直接テンプレートで表示するとリアクティブシステムが働かくなるので注意する。 - Member オブジェクトを算出プロパティにしたため、localNoteを算出する関数内では .value が必要となる。
- v-model を利用して points と連動。
この points もリアクティブ変数なので、この値を変更すると Provide 元の memberList も変化し、結果合計ポイント数もそれに合わせて変動するようになっている。
コメント