【Vue 3入門】 今日から始める本格的フロントエンドWEB開発(コンポーネント編)
Vue.js 3を利用して今後本格的なWebアプリケーションを構築したいという人でこれまで一度もVue.jsを利用した経験がない人に向けて作成した文書です。実際に動作確認しながら学べるようにシンプルなコードを使いながら説明を行っています。
Vue.jsをこれまで使った経験がある人なら気になるところだと思いますがVueではOptions APIとComposition APIという2つの記述方法(API)があります。本文書ではComposition APIの<script setup>を利用してコードを記述していきます。Options APIとCompositon APIの違いやVue2, Vue3の違いなどについては一切触れておりません。プロジェクトの作成はVue CLIではなくViteを利用して行っています。
Vue.jsを使うのが初めての人は公開済みの”基礎編”を先に読むことをお勧めします。
目次
環境の構築
Vue.jsの動作確認を行うためにはCDN、StackBlitz(https://vite.new/vue)を利用することができますがWindows、Macに限らずLinuxでも手元の環境にNode.jsをインストールを行うだけでVue.jsの開発環境を構築することができます。Node.jsはJavaScriptを動かすために必要なJavaScriptエンジン(プログラム)です。Vue.jsに限らずReactやSvelteなどをブラウザ上ではなくOS上でJavaScriptを動作させるために利用します。
Node.jsのインストールが必要となるのでhttps://nodejs.org/ja/にアクセスを行いNode.jsのインストールを行う必要があります。LTSバージョンでも最新版でも動作します。Node.jsをインストールすると一緒にnpm(Node Package Manager)もインストールされます。npmはJavaScriptのパッケージを管理するツールです。本文書でもnpmコマンドを利用してパッケージのインストールを行います。パッケージ管理のツールにはnpmの他にyarn, pnpmなどもあります。どのパッケージ管理のツールを利用してもVue.jsは動作します。
Vue.jsの開発環境はViteを利用して行います。Vite環境を構築するためのコマンドを実行するこことではVue.jsを開発をする際に元になるプロジェクトを作成することできます。Vite本体はビルドツールです。現在Vue.jsでは新規でプロジェクトを作成する場合はVue CLIではなくViteを利用したコマンドを利用することを推奨しています。今後はプロジェクト作成ツールはViteをベースに開発が行われていくようです。
ViteではNode.jsのversion 18+ or 20+なのでnode -vコマンドを実行してNode.jsがインストールされていることを確認しておきます。
% node -v
v20.15.0
任意のプロジェクト名(first-vue-app)を指定してnpmコマンドを利用して Vue. jsのプロジェクトの作成を行います。viteはnpmコマンドのほかにyarn, pnpmコマンドを利用することができます。
% npm create vite@latest [プロジェクト名] -- --template vue
// 実行するコマンド
% npm create vite@latest first-vue-app -- --template vue
実行すると指定したプロジェクト名と同じ名前のフォルダfirst-vue-appが作成されるのでcdコマンドで移動してnpmコマンドを実行します。npm installコマンドを実行することでVue.jsを動作させるために必要なJavaScriptのパッケージが手元のパソコンにインストールされます。
% cd first-vue-app
% npm install
npm installが完了したらnpm run devコマンドでローカルの開発サーバを起動することができます。実行すると開発サーバのURL(http://localhost:3000/)が表示されるのでブラウザからアクセスします。
% npm run dev
//略
VITE v5.3.4 ready in 971 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
ブラウザには下記の画面が表示されます。これでVue.jsの動作確認を行うための環境の構築が完了しました。ローカルサーバに表示される内容を確認しながらアプリケーションを構築していくことになります。
コンポーネントとは
Vue.jsはコンポーネント機能を備えており自分の好きな単位でアプリケーションをコンポーネントとしてパーツ化して作成することができ個別に作成したコンポーネントを組み合わせることで最終的に1つのアプリケーションを作成することができます。コンポーネントの例としてプロジェクト作成時から存在しているHelloWorld.vueファイルがあります。このHelloWorld.vueファイルが1つのコンポーネントに対応しており、初期画面のロゴ以外の部分を表示する役割を持っているコンポーネントです。HelloWorld.vueファイルはコンポーネントとしてパーツ化しているためAppコンポーネントから簡単に取り外すことが可能な上、コンポーネント単位で何度でも再利用することができます。
コンポーネントは好きな単位でコンポーネント化することができるのでヘッダー上にあるナビゲーションメニューをコンポーネント化することもinput要素、ボタンなどより小さな単位でコンポーネント化することも可能です。input要素のコンポーネント化の方法については本文書で説明を行っていますが下記でもコンポーネント化に絞った内容を説明しています。
初めてのコンポーネント
Helloコンポーネントの作成
コンポーネントを作成するためにcomponentsフォルダにHello.vueファイルを作成します。作成したファイルには以下を記述します。
<template>
<h2>初めてのコンポーネント</h2>
</template>
Vueのアプリケーションフォルダの中で拡張子vueがついているファイルにはscript, template, styleタグを記述することができますがコンポーネントとして利用するためには最低templateタグが必要となります。他のタグは省略することができます。
App.vueファイルでHello.vueファイルをimportします。importしたコンポーネントファイルはHelloタグとしてtemplateタグの中で利用することができます。Helloタグとして追加することができるので必要でない場合はHelloタグを削除することができるため簡単に取り外しが可能です。
<script setup>
import Hello from './components/Hello.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello />
</template>
これでコンポーネントを利用するための設定は完了です。script setupを利用した場合のコンポーネントの利用方法を再度確認すると下記の通りの流れになります。。
- コンポーネントファイルを作成
- 別のコンポーネントファイルから作成したコンポーネントファイルをimport
- templateタグの中にimportしたコンポーネントのタグを埋め込む
ブラウザの画面上にはimportしたHelloコンポーネントの内容が表示されます。
コンポーネントの親子関係
importするコンポーネントとimportされるコンポーネントで親子関係を持つことになります。importを行うコンポーネントを親コンポーネントと呼び、importされるコンポーネントを子コンポーネントと呼びます。
コンポーネントの再利用
冒頭のコンポーネントの説明でコンポーネントは再利用できるという説明をしましたが再利用できるという意味がわからない人もいるかもしれないので再利用するとはどういうことか確認しておきましょう。
App.vueファイルの中でimportしたHelloコンポーネントをtemplateタグの中で複数回追加します。
//略
<template>
<h1>Vue 3 入門</h1>
<Hello />
<Hello />
<Hello />
</template>
//略
ブラウザで確認するとtemplateタグに追加したHelloコンポーネントすべてが表示されます。1度コンポーネントをimportするとimportした親コンポーネント側のファイルでは何度でもコンポーネントを利用することができます。
作成したコンポーネントファイルは独立したファイルなので現在のプロジェクト内だけで再利用できるだけではなくファイルをコピー&ペーストすることで全く別のプロジェクトでも利用することができます。
ref、reactive関数の利用
コンポーネントの中ではscriptタグの中でreactivityを持つ変数を定義するためにref, reactive関数を利用します。ref関数でcount, reactive関数でstateを定義しtemplateタグの中でそれぞれの値を表示させます。
<script setup>
import { ref, reactive } from 'vue';
const count = ref(0);
const state = reactive({
count: 1,
});
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>Ref Count:{{ count }}</p>
<p>Reactive Count:{{ state.count }}</p>
</template>
App.vueファイルでHelloコンポーネントをimportしています。
<script setup>
import Hello from './components/Hello.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello />
</template>
設定した通りref, reactive関数で定義した変数に保存された値がブラウザ上に表示されます。
ボタンを追加してそれぞれのcountの数をボタンをクリックで増やせるように設定を行います。
<script setup>
import { ref, reactive } from 'vue';
const count = ref(0);
const state = reactive({
count: 1,
});
const addRefCount = () => {
count.value++;
};
const addReactiveCount = () => {
state.count++;
};
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>Ref Count:{{ count }}</p>
<p>Reactive Count:{{ state.count }}</p>
<div>
<button @click="addRefCount">Ref Count+</button>
<button @click="addReactiveCount">Reactive Count+</button>
</div>
</template>
表示されたボタンをクリックすることでCountの数が増えることがわかります。
App.vueファイルにHelloタグを2つ追加しそれぞれのcountに影響があるかを確認しておきましょう。
<template>
<h1>Vue 3 入門</h1>
<Hello />
<Hello />
</template>
ボタンをクリックするとボタンをクリックしたコンポーネントに含まれる変数のみカcountの数が増えます。
1つのコンポーネントファイルをimportして複数回templateタグの中で利用してもそれぞれのコンポーネントは独立しているためreactiveな変数は他のコンポーネントのボタンの影響を受けることがありません。
コンポーネントは何度も再利用できること、それぞれが独立していることを理解することができました。
データを渡す方法(Props)
ここまでの説明でコンポーネントとは完全に独立したコンポーネントでコンポーネントタグをtemplateタグに組み込むと必ず同じ内容がブラウザ上に表示されることがわかりました。
コンポーネントは独立しておりcountのように内部でreactiveな変数を定義して変数を更新することができましたがそれだけではなく外側からデータを受け取ることができます。データを受け取る際にはProps(properitsの略)を利用します。
Propsの設定方法
Helloコンポーネントにデータを渡したい場合はHelloタグの中に任意の名前の属性名(message)設定し渡したい文字列”Propsの使い方”を設定します。コンポーネント側でデータを受け取る時に設定した名前を利用するので何のデータを渡しているのかわかりやすい名前をつけます。
<template>
<h1>Vue 3 入門</h1>
<Hello message="Propsの使い方" />
</template>
データを受け取る側のHelloコンポーネントの内部ではpropsを受け取るための設定が必要となります。
propsを受け取る場合はdefineProps関数を利用しますが利用する際にimportを行う必要はありません。definePropsの中で受け取るpropsの名前と型を設定します。型には文字列を表すStringの他にNumber, Boolean, Object, Arrayなどを設定することができます。渡される値の型によって設定を変更します。型を設定することでどのようなデータが渡されるかがわかることと異なる型でデータが渡された場合に警告メッセージとして表示されます。どのような警告メッセージが表示されるかは後ほど確認します。definePropsの戻り値を保存する変数名をpropsとしていますが好きな名前をつけることができます。props変数の中にオブジェクトとしてmessageが含まれています。
<script setup>
const props = defineProps({
message:String
})
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>{{ props.message }}</p>
</template>
messageに設定した”Propsの使い方”をHelloコンポーネントで表示することができました。
propsについては型を設定せず配列で名前を指定することで利用することもできます。
<script setup>
const props = defineProps(['message']);
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>{{ props.message }}</p>
</template>
複数のHelloコンポーネントを利用する場合にmessageの値を変更することで異なる内容を表示することができます。
<template>
<h1>Vue 3 入門</h1>
<Hello message="Propsの使い方" />
<Hello message="defineProps関数を利用" />
</template>
Helloコンポーネントを再利用していますがmessageに設定する値を外側から変更することで同じコンポーネントでも異なる内容を表示できるようになりました。
複数のpropsを渡す場合
複数のpropsを渡したい場合の設定方法も確認しておきます。2つのpropsを渡したい場合には名前の異なる属性名を2つ追加してそれぞれに値を設定します。
<template>
<h1>Vue 3 入門</h1>
<Hello message="Propsの使い方" name="Jonh" />
</template>
HelloコンポーネントではdefinePropsに2つのpropsを設定する必要があります。
<script setup>
const props = defineProps({
message: String,
name: String,
});
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>{{ props.name }} {{ props.message }}</p>
</template>
ブラウザで確認すると2つのpropsが表示されていることが確認できます。
definePropsからprops変数に戻されるデータはオブジェクトなので分割代入を利用することもできます。propsの中からname, messageを取り出しています。templateタグでは”props.”を記述する必要がなくなります。
<script setup>
const { name, message } = defineProps({
message: String,
name: String,
});
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>{{ name }} {{ message }}</p>
</template>
複数のpropsの場合も型を設定せず配列で名前を指定することで利用することもできます。
<script setup>
const props = defineProps(['message', 'name']);
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>{{ props.name }} {{ props.message }}</p>
</template>
初期値の設定
HelloコンポーネントのdefinePropsで定義したpropsの一部を親コンポーネントから渡さない場合の動作確認を行います。Hello.vueファイルではdefinePropsでmessage, nameを設定しています。
<script setup>
const props = defineProps({
message: String,
name: String,
});
</script>
HelloコンポーネントではdefinPropsで2つのpropsを設定していますがAppコンポーネントではHelloタグにmessage属性だけ設定しています。
<template>
<h1>Vue 3 入門</h1>
<Hello message="Propsの初期値の設定" />
</template>
上記の設定後ブラウザで確認するとエラーメッセージが出ることもなくmessageに設定した”Propsの初期値の設定”のみ表示されます。
nameを設定しなくてもエラーメッセージは出ませんでしたがpropsには初期値を設定することができます。初期値が設定されているpropsが渡されなかった場合には初期値が利用されます。初期値を設定する場合はオブジェクトの中にdefaultプロパティを設定して初期値を設定します。propsの設定をオブジェクトに変更した場合の型の設定はtypeプロパティで行います。
nameのみ初期値の設定を行っています。
<script setup>
const props = defineProps({
message: String,
name: { type: String, default: 'Jane' },
});
</script>
先ほどと同様にHelloコンポーネントにはnameを渡さずに設定を行います。
<template>
<h1>Vue 3 入門</h1>
<Hello message="Propsの初期値の設定" />
</template>
AppコンポーネントからHelloタグにpropsのnameを設定していませんがdefinePropsで設定したデフォルト値が表示されることが確認できます。
definePropsに設定したpropsを設定しなくてもエラーがでませんでしたが場合によってはpropsを必ず受け取りたい場合もあります。その場合にはpropsが必須かどうか指定できるrequiredプロパティを利用することができます。defineProps関数の中のpropsのmessageのrequiredプロパティをtrueを設定します。
<script setup>
const props = defineProps({
message: {
type: String,
required: true,
},
name: { type: String, default: 'Jane' },
});
</script>
下記のAppコンポーネントではHelloタグにpropsの設定を行っていません。
<template>
<h1>Vue 3 入門</h1>
<Hello />
</template>
ブラウザ上にはnameの初期値に設定したJaneが表示されますがブラウザのデベロッパーツールのコンソールには下記の警告メッセージが表示されます。requiredをtrueに設定することでpropsのmessageが渡されていないことがわかります。
[Vue warn]: Missing required prop: "message"
at <Hello>
at <App>
デフォルトではrequiredをfalseになっているのでtrueからfalseに設定すると警告メッセージが表示されなくなります。
数値を渡したい場合
文字列ではなく数値を渡したい場合のpropsの設定を確認します。数値を受け取りたい場合はtypeにNumberを設定することができます。templateタグの中では受け取った値に対して100を足すだけのシンプルなコードで数値が渡されているのかどうか確認します。
<script setup>
const props = defineProps({
price: Number,
});
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p>{{ props.price + 100 }}</p>
</template>
Helloに対してこれまでと同じ方法でpropsのprice属性を利用して1000を渡します。
<template>
<h1>Vue 3 入門</h1>
<Hello price="1000" />
</template>
ブラウザを見ると1100ではなく1000100と文字列の連結が行われていることがわかります。”+”は数値の場合は足し算を行いますが文字列の場合は文字列の結合を行います。
コンソールを確認すると警告メッセージが表示されていることがわかります。
メッセージの内容を見るとpropsのpriceは数値の値となるはずが文字列の1000が渡されているということがわかります。文字列と同じ方法では数値を渡すことができないことがわかりました。
propsで数値を渡したい場合はその値が数値であることを示すためにv-bindの設定を行います。priceに対してv-bindを設定します。
<template>
<h1>Vue 3 入門</h1>
<Hello v-bind:price="1000" />
</template>
v-bind:をpriceに設定後に再度確認すると足し算が行われ1100が表示され警告メッセージの表示も解消していることがわかります。
v-bindは省略して:priceと設定することができます。
<template>
<h1>Vue 3 入門</h1>
<Hello :price="1000" />
</template>
Booleanを渡したい場合
同じ例としてtrue, falseの真偽値のBooleanの場合も確認しておきます。isAdminというpropsを定義してtypeをBooleanとします。isAdminの値によってv-ifディレクティブの分岐を利用して表示内容を変えます。
<script setup>
const props = defineProps({
isAdmin: Boolean,
});
</script>
<template>
<h2>初めてのコンポーネント</h2>
<p v-if="props.isAdmin">管理者です。</p>
<p v-else>管理者ではありません。</p>
</template>
HelloコンポーネントにisAdminでfalseを設定します。
<template>
<h1>Vue 3 入門</h1>
<Hello isAdmin="false" />
</template>
v-ifディレクティブの分岐により”管理者ではありません。”と表示されると思いますが”管理者です。”が表示され、警告メッセージがコンソールに表示されます。booleanの値が渡されるはずなのにStringで”false”が渡されているといった内容のメッセージです。
解決方法は数値の場合と同じでv-bindを設定することでfalseがBooleanとして渡され”管理者ではありません。”が表示されます。
<template>
<h1>Vue 3 入門</h1>
<Hello v-bind:isAdmin="false" />
</template>
propsを利用して数値、Booleanなど期待通りに動作しない場合はv-bindが設定されているのか確認を行ってください。
reactiveな変数を渡したい場合
親コンポーネントで定義したreactiveな変数をpropsを利用して子コンポーネントに渡す方法を確認していきます。
App.vueファイルにref関数で変数nameを定義します。Helloタグにname属性を追加した定義したname変数を設定します。
<script setup>
import Hello from './components/Hello.vue';
import { ref } from 'vue';
const name = ref('John');
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello name="name" />
</template>
子コンポーネントのHello.vueファイルではpropsのnameは文字列なので型をStringに設定しています。
<script setup>
const props = defineProps({
name: String,
});
</script>
<template>
<h2>子コンポーネント</h2>
<p>Hello {{ props.name }}</p>
</template>
ブラウザで確認するとname propsに設定した文字列nameがそのまま表示されます。ref関数の初期値に設定した”John”が表示されません。
ただのnameという文字列ではなくnameという変数を渡すためにはここでもv-bindを利用する必要があります。name propsの前に:(コロン)をつけます。v-bind:でも大丈夫です。
<template>
<h1>Vue 3 入門</h1>
<Hello :name="name" />
</template>
v-bindを設定して保存した瞬間にブラウザ上のnameがref関数で設定した初期値”John”へと変わります。
reactiveな変数をpropsで利用したい場合にはv-bindを利用する必要があることがわかりました。
reactiveな変数を更新
reativeな変数なので親コンポーネントで値を変更した場合に子コンポーネントで表示されている値が更新されるのかを確認します。
clickイベントを持つボタンを追加し、ボタンをクリックするとchangeName関数によりnameの値が”John”から”Jane”に更新します。
<script setup>
import { ref } from 'vue';
import Hello from './components/Hello.vue';
const name = ref('John');
const changeName = () => {
name.value = 'Jane';
};
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello :name="name" />
<button @click="changeName">Change Name</button>
</template>
最初は”John”と表示されていますがボタンをクリックすると”Jane”に変更されることが確認できます。reactiveな変数を親コンポーネントで更新するとpropsで渡されたコンポーネント側でのその更新が反映されることがわかりました。
アプリケーションを構築していくと必ずpropsで渡されたreactiveな変数を子コンポーネント側で更新したいという場面に出くわします。
渡されたpropsを子コンポーネントで直接更新してはいけません。
”渡されたpropsを子コンポーネントで直接更新してはいけません”というルールを理解を深めるために子コンポーネントでpropsを更新できるか確認しておきましょう。
<script setup>
const props = defineProps({
name: String,
});
const changeName = () => {
props.name = 'Ken';
};
</script>
<template>
<h2>子コンポーネント</h2>
<p>Hello {{ props.name }}</p>
<button @click="changeName">Change Name</button>
</template>
ボタンをクリックするとコンソールにはreadonlyなので更新が行えないと警告メッセージが表示されます。
Set operation on key "name" failed: target is readonly. Proxy{name: 'John'}
警告メッセージが表示されるので誤って更新することはないのではないかと思いますが次はpropsで渡す値をオブジェクトに変更します。変数名をnameからpersonに変更しています。
<script setup>
import { ref } from 'vue';
import Hello from './components/Hello.vue';
const person = ref({
name: 'John',
});
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello :person="person" />
</template>
propsで受け取る型をStringからObjectに変更しています。
<script setup>
const props = defineProps({
person: Object,
});
const changeName = () => {
props.person.name = 'Ken';
};
</script>
<template>
<h2>子コンポーネント</h2>
<p>Hello {{ props.person.name }}</p>
<button @click="changeName">Change Name</button>
</template>
ボタンをクリックすると警告メッセージが表示されることなく更新は完了します。警告メッセージが表示されないので注意する必要があります。では子コンポーネントで行う何かしらのアクションを元に親コンポーネントで定義されているpropsの値を更新したい場合はどのように行えばよいのでしょうか。
子コンポーネントで親コンポーネントで定義したreactiveな変数を更新したい場合にはemitイベントを利用します。emitイベントについては後ほど説明しますがここで簡単に説明しておくとemitイベントを子コンポーネントで発火させ親コンポーネントがそのイベントを検知して親コンポーネントで変数を更新します。
class属性の設定を渡したい場合
コンポーネントではpropsを利用せずにid属性やclass属性などコンポーネントタグに設定した属性を渡すことができます。この機能の名前は”fallthrough attribute” といいます。
Helloタグににclass属性を使ってactiveクラスを設定します。
<script setup>
import Hello from './components/Hello.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello class="active" />
</template>
これまでの動作確認の理解すると子コンポーネントであるHelloでdefinePropsで受け取るclassの設定を行うのではと思いますがclass属性はpropsとは別の扱いになるため設定を行う必要がありません。
Helloコンポーネントのtemplateがどのように記述されているのか確認を行います。
<template>
<h2>子コンポーネント</h2>
</template>
h2タグのみ含まれていると確認できたのでブラウザのデベロッパーツールで要素を確認します。
子コンポーネントのtempleteタグ内で記述したh2タグにHelloタグで設定したclassのactiveクラスがそのまま設定されていることが確認できます。class属性は何の追加設定もなく子コンポーネントに渡すことができることがわかりました。
子コンポーネントのh2タグにはclass属性を設定していませんでしたがclass属性が設定されている場合にはどのような動作になるのか確認します。
<template>
<h2 class="info">子コンポーネント</h2>
</template>
その場合はHelloタグで設定したclassとh2タグで設定したclassがマージされて設定されます。どちらかが上書きされることがないこと、class属性はpropsとは別の仕組みで子コンポーネントに渡されることがわかりました。
ここで子コンポーネントのルート要素が2つの要素が構成されている場合はどのなるのか疑問を持っている人もいるかもしれないので確認します。2つの要素が存在するとはtemplateタグの直下にh2タグとpタグが2つある状況です。先ほどはh2タグが1つだけ存在しました。
<template>
<h2 class="info">子コンポーネント</h2>
<p>class属性の渡し方確認中</p>
</template>
デベロッパーツールで要素を確認します。activeクラスがどこにも設定されていません。子コンポーネントにおけるルートの要素が2つの場合にはコンポーネントで設定したclassはどちらの要素にも設定されないということがわかりました。
その2つの要素をdivで囲んだ場合はルートの要素が1つになり動作確認を行ったh2タグの状況と同じになるためclassのactiveが設定されます。
<template>
<div>
<h2 class="info">子コンポーネント</h2>
<p>class属性の渡し方確認中</p>
</div>
</template>
デベロッパーツールで確認するとdiv要素にclass=”active”が設定されていることがわかります。
Helloタグで設定したclass属性をそのまま子コンポーネントのタグに指定したい場合には$attrsを利用することができます。pタグでclass属性にv-bindを設定し$attrs.classを設定します。
<template>
<div>
<h2 class="info"gt;子コンポーネント</h2>
<p :class="$attrs.class">class属性の渡し方確認中</p>
</div>
</template>
デベロッパーツールのpタグにはclass属性が設定されていることがわかります。div要素に設定されたclassはそのまま適用されています。$attrsの中には親コンポーネントから渡されたclasss属性の情報が含まれているので$attrsを利用することで子コンポーネントのルート要素の1箇所だけではなく何箇所にでも設定ができます。
ルートの要素にclassを適用させたくない場合はscriptタグをscript setupタグとは別に追加し、inhertAttrsパラメータをfalseにすることで対応することができます。下記のコードではdivタグへのclassの適用はなくなり、$attrs.classを設定しているpタグのみにclassが適用されます。
<script>
export default {
inheritAttrs: false,
};
</script>
<script setup></script>
<template>
<div>
<h2 class="info">子コンポーネント</h2>
<p :class="$attrs.class">style属性の渡し方確認中</p>
</div>
</template>
useAttrsによる属性の取得
$attrsを利用することで子コンポーネントのタグに設定したclass属性の設定値を子コンポーネントで利用できることがわかりました。scriptタグないで$attrsに含まれる値を確認したい場合にあuseAttrs APIを利用することができます。
<script setup>
import { useAttrs } from 'vue';
const attrs = useAttrs();
console.log(attrs);
</script>
コンソールを見るとclassにactiveが設定されていることが確認できます。
Proxy{class: 'active', __vInternal: 1}
attrsを利用してclassを適用したい場合は下記のように行うことができます。
<p :class="attrs.class">class属性の渡し方確認中</p>
useAttrsを使ってclass属性以外のid, style属性を子コンポーネントに渡すことができるか確認します。
<Hello id="main" style="color:red" class="active" />
useAttrの内容からstyle属性を利用したい場合には下記のように行えることがわかります。
<template>
<h2 class="info">子コンポーネント</h2>
<p :style="attrs.style">style属性の渡し方確認中</p>
</template>
styleタグではcolor:redを設定していたのでattrs.styleを設定した要素の文字は赤くなります。
propsを利用せずid, class, style属性を親コンポーネントから子コンポーネントに渡せること、利用方法を理解することができました。
子から親へのイベントによる通知
propsでは常に親コンポーネントから子コンポーネントに対してデータを渡すというものでした。propsとして渡されたデータは子コンポーネントでは直接更新してはいけないというルールがあります。そのため親コンポーネントが持つreactiveな変数は親コンポーネントで更新しなければなりません。もし子コンポーネントで行うユーザのアクションによって親のreactiveな変数を更新する必要が出た場合はどうすればいいのでしょうか。その場合、子コンポーネントから親コンポーネントに対してとpropsで受け取ったreactiveな変数を更新して欲しいと伝えるための通知の仕組みが必要となります。その仕組みに利用するのがemitです。emitではイベントを利用し子コンポーネントから親コンポーネントに通知を行います。emitはイベントと一緒に親コンポーネントにデータを渡すことも可能です。propsが親から子へとデータを渡すのに利用されるのとは逆でemitは子から親へデータを渡す際に利用されます。emitを利用して親から子へデータを渡したり通知に使うことはできません。
$emitによるイベント発生と検知
子コンポーネントから親コンポーネントへの通知の設定方法を$emitを利用して確認します。子コンポーネントにはHello.vueファイルを利用します。
子コンポーネントのtemplateタグの中から親コンポーネントに通知を行いたい場合は$emitの引数に任意の名前のイベント名を設定して実行するだけです。イベントを発生させるためには何かイベントを発生させるためのトリガーとなるアクションが必要です。イベントを設定しただけでは何も起こりません。ユーザが行うアクションに対して$emit関数を実行させるためボタンにclickイベントを設定しています。ボタンをクリックすると$emitの引数に設定したnotificationというイベントが発生します。notificationはイベント名で任意の名前をつけることができますが発生したイベントを親コンポーネントで検知する際に利用するのでわかりやすい名前をつけます。
<script setup>
</script>
<template>
<div>
<h2>子コンポーネント</h2>
<button @click="$emit('notification')">通知</button>
</div>
</template>
JavaScript、Vueに限らずイベントが発生した場合そのイベントを検知するための設定が必要となり、子コンポーネントHelloで発生したイベントは親コンポーネントAppで検知する必要があります。イベントの検知は親コンポーネントで設置した子コンポーネントのタグの中で行います。
イベントの検知にはv-onディレクティブを利用しv-on:の後に$emit関数の引数に設定した名前を設定します。ここではnotificationになります。イベントを検知した後に実行する関数をhandleEventで設定しscriptタグの中にhandleEvent関数の処理を記述しています。
<script setup>
import Hello from './components/Hello.vue';
const handleEvent = () => {
console.log('子コンポーネントからの通知');
};
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello v-on:notification="handleEvent" />
</template>
画面には”通知”ボタンが表示されるのでボタンをクリックしてください。コンソールにhandleEvent関数で設定した”子コンポーネントからの通知”が表示されたら子コンポーネントで発生したイベントが親コンポーネントが正常に検知したことになります。
$emitを利用した場合に上記のコードでは子コンポーネントのtemplateタグでdiv要素で全体をラップしていました。そのdivを外します。
<template>
<h2>子コンポーネント</h2>
<button @click="$emit('notification')">通知</button>
</template>
div要素を外しルート要素が複数になった場合は下記の警告メッセージが表示されます。警告メッセージなので今回のコードであれば問題なく動作は行われますのでdiv要素を追加することでルート要素を1つにするかこの後に説明を行うdefineEmits関数でイベントを定義しておくと警告メッセージは消えます。
runtime-core.esm-bundler.js:6620 [Vue warn]: Extraneous non-emits event listeners (notification) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.
at <Hello onNotification=fn<handleEvent> >
at <App>
defineEmits関数でnotificationを定義しておくと警告メッセージは消えます。
<script setup>
defineEmits(['notification']);
</script>
<template>
<h2>子コンポーネント</h2>
<button @click="$emit('notification')">通知</button>
</template>
defineEmits関数には戻り値を保存することができます。下記のように戻り値をemitに保存した場合はemit関数として利用することができます。その場合は$emitを利用せずemit(‘notification’)を利用することができますがscriptタグで定義したemitと同じ名前に設定する必要があります。
<script setup>
const emit = defineEmits(['notification']);
</script>
<template>
<h2>子コンポーネント</h2>
<button @click="emit('notification')">通知</button>
</template>
defineEmitsを利用した場合
Helloコンポーネントからnotificationイベントを発生する際にtemplateタグの中では$emit関数を利用していましたがscriptタグの中の関数を利用してイベントを発生する際にはdefineEmits関数を利用します。defineEmits関数の引数では配列でイベント名の設定を行います。実行する際にはdefineEmitsの戻り値の関数emitに引数のイベント名を設定して実行します。
<script setup>
const emit = defineEmits(['notification']);
const sendNotification = () => {
emit('notification');
};
</script>
<template>
<h2>子コンポーネント</h2>
<button @click="sendNotification">通知</button>
</template>
ボタンを押すとコンソールには”子コンポーネントからの通知”が表示されます。
下記のように記述した場合にdefineEmitsにイベント名を設定しなくても動作しますがdefineEmitsでイベント名を設定するようにしましょう。
<script setup>
const emit = defineEmits();
const sendNotification = () => {
emit('notification');
};
</script>
<template>
<div>
<h2>子コンポーネント</h2>
<button @click="sendNotification">通知</button>
</div>
</template>
emitイベントを利用した更新
emitによって発生したイベントの通知を利用して親コンポーネントで定義したreactiveな変数の更新方法を確認します。
App.vueファイルの中でref関数を利用してreactiveな変数nameを定義します。propsを使ってHelloコンポーネントにnameを渡します。子コンポーネントHelloでこの後に定義を行うchangeNameEventイベントを検知してhandleEvent関数を実行しnameの値を”Ken”に更新します。
<script setup>
import { ref } from 'vue';
import Hello from './components/Hello.vue';
const name = ref('John');
const handleEvent = () => {
name.value = 'Ken';
};
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello @changeNameEvent="handleEvent" :name="name" />
</template>
HelloコンポーネントではボタンをクリックするとchangeName関数を実行し、changeName関数の中ではchangeNameEventイベントを発生させます。
<script setup>
const props = defineProps({
name: String,
});
const emit = defineEmits(['changeNameEvent']);
const changeName = () => {
emit('changeNameEvent');
};
</script>
<template>
<h2>子コンポーネント</h2>
<p>Hello {{ props.name }}</p>
<button @click="changeName">Change Name</button>
</template>
設定は完了です。
“Change Name”ボタンをクリックする前はpropsで受け取ったnameの初期値であるJohnが画面上に表示されています。
“Change Name”ボタンをクリックするとchangeNameEventが発生して親コンポーネントでchangeNameEventイベントを検知してhandleEvent関数を実行し、reactiveな変数であるnameの値を”John”から”Ken”に更新しています。
emitを利用することで子コンポーネントで行われたユーザのアクションを元に親コンポーネントのreactiveの変数を更新する方法を理解することができました。子コンポーネントはイベントで通知を行うだけで実際に更新を行うのは更新が許されている通知を受け取った親コンポーネントです。
emitでデータを渡す
emitを利用することでイベントを発生させることができました。emitはイベント発生によって親コンポーネントに通知するだけではなくてデータを渡すこともできます。emitの第一引数にはイベント名を設定していましたがemitの第二引数に親コンポーネントに渡したいデータを設定することができます。
emitを実行する子コンポーネンではchangeNameEventと一緒に”Kevin”という渡したいデータを設定しています。
const changeName = () => {
emit('changeNameEvent','Kevin');
};
イベントを受け取る親コンポーネントでは関数の引数(newName)からemitで設定したデータを取得することができます。
<script setup>
import { ref } from 'vue';
import Hello from './components/Hello.vue';
const name = ref('John');
const handleEvent = (newName) => {
name.value = newName;
};
</script>
<template>
<h1>Vue 3 入門</h1>
<Hello @changeNameEvent="handleEvent" :name="name" />
</template>
ボタンをクリックするとemitで設定した”Kevin”がブラウザ上に表示されます。
複数の値を渡したい場合にはオブジェクトを利用することができます。
const changeName = () => {
emit('changeNameEvent', { firstName: 'Kevin', lastName: 'James' });
};
親コンポーネントのAppではデータはオブジェクトとして渡されるので下記のように設定を行うことができます。
const handleEvent = (newName) => {
name.value = `${newName.firstName} ${newName.lastName}`;
};
ここまでの動作確認でemitでデータを渡すことができることがわかったので”Kevin”といった固定値ではなくinput要素に入力したデータが渡せるようにinput要素を追加します。渡すデータの設定なので子コンポーネントで設定を行います。
ref関数でreactiveな変数nameを定義してinput要素にv-modelディレクティブでnameを設定します。input要素に入力を行うと文字入力/削除毎に反映させれるようにinputイベントを利用しています。name変数の初期値にはpropsから渡されるprops.nameを設定しています。
<script setup>
import { ref } from 'vue';
const props = defineProps({
name: String,
});
const name = ref(props.name);
const emit = defineEmits(['changeNameEvent']);
const changeName = () => {
emit('changeNameEvent', name.value);
};
</script>
<template>
<h2>子コンポーネント</h2>
<p>Hello {{ props.name }}</p>
<input type="text" v-model="name" @input="changeName" />
</template>
親コンポーネントAppではイベントを検知しイベントと一緒に渡されるデータを受け取るという処理は同じなので変更はありません。
ブラウザで確認するとinput要素にはpropsで渡されたnameの初期値”John”が設定されています。
input要素の文字を更新すると表示されている文字も一緒に更新されます。文字を変更する度にイベントが発生して親コンポーネントがイベントを検知して更新処理を行い、更新した値がpropsを経由して子コンポーネントに渡され画面に反映されています。input要素で入力した値を使って親コンポーネントのreactiveな変数を更新する方法を理解することができました。
子コンポーネントでref関数を利用せず$eventオブジェクトから取得した値をイベントと一緒に渡すことでも対応することができます。emitで渡したデータを利用した更新された値はpropsを経由してinput要素のvalue属性をv-bindすることで反映させています。
<script setup>
const props = defineProps({
name: String,
});
const emit = defineEmits(['changeNameEvent']);
</script>
<template>
<!-- <div></div> -->
<h2>子コンポーネント</h2>
<p>Hello {{ props.name }}</p>
<input
type="text"
:value="props.name"
@input="$emit('changeNameEvent', $event.target.value)"
/>
</template>
input要素をコンポーネント化
本文書の冒頭のコンポーネントの説明でコンポーネントは自分の好きな単位でコンポーネント化できるという話をしました。
ここではinput要素をコンポーネント化する方法を確認していきます。input要素をコンポーネント化することでinput要素を複数持つ入力フォームがある場合にコンポーネント内でスタイルを設定し再利用することで統一性のあるデザインの入力フォームを作成することができます。input要素を個別に同じスタイルを設定することもできますがコンポーネント化することで変更が必要になった場合にコンポーネントを更新するだけですべてのinput要素に反映させることができます。
input要素をコンポーネント化する前にv-modelディレクティブはvalueと@inputイベントをわけて設定できることを確認します。実際はv-modelを2つに分けれるのではなく本来はvalueと@inputイベントを利用する設定を簡易的にするためにv-modelディレクティブが作られています。
<input type="text" v-model="name" />
v-modelからvalueと@inputの書式に変更します。v-modelを利用しなくても下記の書式でもv-modelと同じ動作になります。
<input type="text" :value="name" @input="name = $event.target.value" />
上記の書式を元にInputコンポーネントを作成するためにcomponensフォルダにInput.vueファイルを作成します。作成したファイルではpropsを使ってinput要素のvalueに設定値modelValueを定義します。inputイベントの中では$emitの第一引数にイベント名のupdate:modelValueを設定し、第二引数にinput要素に入力した値を設定します。Inputコンポーネントではinput要素の入力するとupdate:modelValueが発生し、イベントと一緒にinput要素に入力したデータが親コンポーネントに渡されます。Inputコンポーネントでは親からmodalValueを受け取り、親にupdate:modalValueイベントで更新を通知しています。
<script setup>
const props = defineProps({
modelValue: String,
});
</script>
<template>
<input
:value="props.modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
Inputコンポーネントをimportする親コンポーネントのApp.vueファイルではref関数でnameを定義しInputコンポーネントで設定したpropsのmodal-valueにnameを設定します。これnameの値がpropsを経由してinput要素のvalueに設定されます。
import { ref } from 'vue';
import Input from './components/Input.vue';
const name = ref('John Doe');
</script>
<template>
<h1>Vue 3 入門</h1>
<p>name:{{ name }}</p>
<Input :model-value="name" />
</template>
子コンポーネントはpropsを通してnameを受け取っていますがinput要素に文字を入力しても変化はありません。それは子コンポーネントで発生したイベントを検知する設定を親コンポーネントで行っていないためです。
子コンポーネントから発生するupdate:modelValueイベントを検知するためInputタグに@update:model-valueを追加しnameにemitの引数で設定した$event.target.valueを受け取れるように$eventを設定します。
<Input :model-value="name" @update:model-value="name = $event" />
イベント名に:(コロン)が入っていたり$eventオブジェクトを利用しているので複雑に見えますがこれまで学んだpropsとemitを利用しているだけです。」これでinput要素のコンポーネント化は完了です。
input要素に変更を加えるとブラウザ上に表示されているnameの値が更新されます。
input要素がコンポーネント化できたのでInputコンポーネントにスタイルを設定します。
<script setup>
const props = defineProps({
modelValue: String,
});
</script>
<template>
<input
:value="props.modelValue"
@input="$emit('update:modelValue', $event.target.value)"
style="padding: 1em; border-radius: 1em; margin: 0.5em"
/>
</template>
スタイルを適用したのでinput要素の見栄えが変わります。
input要素をコンポーネント化の利点は再利用する場合です。同じデザインのinput要素を表示する場合はApp.vueファイルではimportが完了しているのでreactiveな変数を別に定義してInputコンポーネントに設定します。
<script setup>
import { ref } from 'vue';
import Input from './components/Input.vue';
const name = ref('John Doe');
const address = ref('');
</script>
<template>
<h1>Vue 3 入門</h1>
<p>name:{{ name }}</p>
<p>address:{{ address }}</p>
<Input :model-value="name" @update:model-value="name = $event" />
<Input :model-value="address" @update:model-value="address = $event" />
</template>
同じデザインのinput要素が表示され入力すると入力した内容が即座に表示されます。コンポーネント化しているので同じデザインのinput要素を利用したい場合はInputコンポーネントを利用することで実現できます。もしデザインを変更したい場合はInputコンポーネントを更新することで利用しているすべてのinput要素に反映されます。
さらにInputタグではv-modelを利用することができるので下記のようにシンプルに記述することができます。
<template>
<h1>Vue 3 入門</h1>
<p>name:{{ name }}</p>
<p>address:{{ address }}</p>
<Input v-model="name" />
<Input v-model="address" />
</template>
input要素のコンポーネント化の設定方法を理解することができました。
バリデーションの設定
defineEmitsで定義したイベントはイベント名だけを設定した配列からオブジェクトに変更することでイベントと一緒に親コンポーネントに渡すデータの値が妥当かどうかのバリデーションのチェック機能を追加することができます。
Helloコンポーネントに設定したchangeNameEventイベントを使ってバリデーションの設定を行います。defineEmitsの引数でこれまで配列でイベントの名前が登録されていましたがオブジェクトに変更しオブジェクトのプロパティにイベント名を設定し値に関数を設定します。その関数の中でバリデーションを行うことができ、関数の戻り値にtureかfalseを設定する必要があります。下記では引数のnameを利用してinput要素に入力した値をconsole.logでコンソールに表示しています。バリデーションは行っていませんが戻り値をtrueに設定しているので動作としてはバリデーションに問題はなかったことになります。falseが戻されるとバリデーションに問題があることになります。
<script setup>
import { ref } from 'vue';
const props = defineProps({
name: String,
});
const name = ref(props.name);
const emit = defineEmits({
changeNameEvent: (name) => {
console.log(name);
return true;
},
});
const changeName = () => {
emit('changeNameEvent', name.value);
};
</script>
<template>
<h2>子コンポーネント</h2>
<p>Hello {{ props.name }}</p>
<input type="text" v-model="name" @input="changeName" />
</template>
設定後にinput要素に入力すると入力した文字がコンソールに表示されます。バリデーションに問題があった場合を想定して戻り値をfalseにするとコンソールには警告メッセージが表示されます。
const emit = defineEmits({
changeNameEvent: (name) => {
console.log(name);
return false;
},
});
メッセージにはバリデーションがchangeNameEventでバリデーションに失敗したと表示されています。
[Vue warn]: Invalid event arguments: event validation failed for event "changeNameEvent"
戻り値をtrueかfalseにするかでメッセージ表示されるかどうかが確認できたので実際にバリデーションのコードを追加します。バリデーションではnameに値が入っているかどうかのチェックを行っています。
const emit = defineEmits({
changeNameEvent: (name) => {
if (name) return true;
return false;
},
});
input要素を空白にした時ののみ警告メッセージがコンソールに表示されます。emitで利用するイベントと一緒に渡されるデータのバリデーション方法を理解することができました。
Slot
親から子コンポーネントに値を渡す場合にPropsを利用することができましたがHTMLなどのコンテンツを渡したい場合にはSlotを利用することができます。
Slotのシンプルな例
componetsフォルダにUser.vueファイルを作成します。親コンポーネントから受け取ったコンテンツを表示したい場所にslotタグを設定します。
<template>
<p>このユーザの名前は<slot></slot>です。</p>
</template>
App.vueファイルではUserコンポーネントをimportしてUserタプの間にコンテンツを挿入します。
<script setup>
import User from './components/User.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<User>John Doe</User>
</template>
Userタグの間に挿入した”John Doe”がUserコンポーネントのtemplateタグで設定したslotの場所に表示されていることが確認できます。このようにSlotを利用することができます。
このようにコンテンツが文字列のようなシンプルな場合はpropsでも対応することができます。
<script setup>
const props = defineProps(['name']);
</script>
<template>
<p>このユーザの名前は{{ props.name }}です。</p>
</template>
<template>
<h1>Vue 3 入門</h1>
<User name="John Doe" />
</template>
propsとの違いも明確にするためSlotを利用することで文字列ではなくHTMLも設定できることを確認しておきます。Userコンポーネントは最初の例のものを利用します。
<template>
<p>このユーザの名前は<slot></slot>です。</p>
</template>
今回はUserタグの間にspanタグにstyle属性が設定されたコンテンツを挿入します。
<script setup>
import User from './components/User.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<User>
<span style="font-weight: 900; font-size: 1.4em">John Doe</span>
</User>
</template>
style属性が適用された”John Doe”を確認することができます。
spanのコンテンツをpropsに設定するとそのままhtmlタグが表示されます。
初期値の設定
slotタグに初期値を設定したい場合はslotタグの間にコンテンツを挿入しておくことで初期値として表示されます。Userコンポーネントのslotタグの間にspanタグで構成されたコンテンツを挿入しています。
<template>
<p>
このユーザの名前は<slot
><span style="font-weight: 900; font-size: 1.4em; color: red"
>John Doe</span
></slot
>です。
</p>
</template>
親コンポーネントではUserタグの中には何も挿入しません。
<template>
<h1>Vue 3 入門</h1>
<User />
</template>
slotタグの間に挿入したコンテンツが初期値として利用されます。
slotに初期値を設定した場合でも親コンポーネントでタグの間にコンテンツを挿入した場合は親コンポーネントでの設定が優先され表示されます。
<template>
<h1>Vue 3 入門</h1>
<User><span style="color: blue">Jane Doe</span></User>
</template>
複数のSlotsの設定
Userコンポーネントではslotの設定場所は1箇所でしたが複数箇所設定することもできます。複数Slotを設定した場合の設定方法について確認していきます。
slotを複数設定する場合はそれぞれのslotを識別するためのIDである名前が必要となります。そのためNamed Slot(名前付きSlot)と呼ばれます。名前をつける場合はslotタグのname属性にコンポーネント内で一意となる名前をつけます。この名前は親コンポーネントからどの場所にコンテンツを挿入するか指定する際に利用されます。
<template>
<div>
<slot name="title"></slot>
</div>
<div>
<slot name="content"></slot>
</div>
<div>
<slot name="actions"></slot>
</div>
</template>
Named Slotにコンテンツを渡す親コンポーネントではtemplateタグにv-slotを設定します。v-slotにはUser.vueコンポーネントでつけたnameを指定します。
<template>
<h1>Vue 3 入門</h1>
<User>
<template v-slot:title><h1>ユーザ情報</h1></template>
<template v-slot:content
><div>
<div>John Doe</div>
<div>Jane Doe</div>
</div></template
>
<template v-slot:actions><button>ユーザ追加</button></template>
</User>
</template>
v-slotに設定したnameの場所にそれぞれで設定したコンテンツが表示されます。
Named Slotを利用した場合でも明示的にname属性を設定しないslotタグがdefaultとなります。name属性を設定しなくてもコンテンツを受け取ることができます。真ん中のslotからname=”content”を削除します。
<template>
<div>
<slot name="title"></slot>
</div>
<div>
<slot></slot>
</div>
<div>
<slot name="actions"></slot>
</div>
</template>
デフォルトのslotにコンテンツを渡す場合はtemplateタグのv-slotを利用する必要がなくUserタグの中に挿入したコンテンツがそのままデフォルトのslotに設定されます。v-slotがないtemplateタグで囲んでもデフォルトのslotには設定されません。
<template>
<h1>Vue 3 入門</h1>
<User>
<template v-slot:title><h1>ユーザ情報</h1></template>
<div>
<div>John Doe</div>
<div>Jane Doe</div>
</div>
<template v-slot:actions><button>ユーザ追加</button></template>
</User>
</template>
もしデフォルトのslotを明示的にv-slotを使って設定したい場合にはdefaultを設定します。
<template v-slot:default>
<div>
<div>John Doe</div>
<div>Jane Doe</div>
</div></template
>
Named Slotの場合もUserコンポーネントのslotタグの間にコンテンツを挿入することでデフォルト値として設定することができます。
<template>
<div>
<slot> name="title">ユーザ</slot>
</div>
<div><slot>コンテンツ</slot></div>
<div>
<slot> name="actions">アクション</slot>
</div>
</template>
AppコンポーネントのUserタグでコンテンツを挿入しない場合はデフォルト値が表示されます。
<template>
<h1>Vue 3 入門</h1>
<User />
</template>
Scoped Slotの設定
Scoped Slotはslotを利用して子コンポーネント側からデータを渡し親コンポーネントからそのデータにアクセスを行うことができます。この機能については動作確認したほうがわかりやすいのシンプルな例を使って説明します。
子コンポーネント側でslotタグの中にpropsと同様に任意の名前の属性を追加し値を設定します。ここではmessageという名前をつけて値にユーザを設定しています。このmessage属性のことをslot propsと呼びます。
<template>
<slot message="ユーザ"></slot>
</template>
親コンポーネントではslot propsの値にアクセスするためにtemplateタグを利用します。defaultのslotからslot propsを受け取るのでv-slot:defaultに任意の名前を設定します。ここではslotPropsという名前をつけています。slotPopsの中にUserコンポーネントで設定したslot propsであるmessageが含まれているか確認するためslotPropsをマスタッシュで囲みます。
<template>
<h1>Vue 3 入門</h1>
<User>
<template v-slot:default="slotProps">
{{ slotProps }}
</template>
</User>
</template>
ブラウザで確認するとslot propsの中身を確認することができ子コンポーネントのslotタグで設定したmessageとその値を確認することができます。
親コンポーネントではv-slot:defaultを設定したタグの中でslotPropsにアクセスを行いmessageに設定した値を表示することができます。ブラウザには”ユーザ”が表示されます。
<template>
<h1>Vue 3 入門</h1>
<User>
<template v-slot:default="slotProps">
{{ slotProps.message }}
</template>
</User>
</template>
Named Slotによる複数のslotの設定と1つのslotに複数のslot propsが設定できるかの確認を行います。name属性にheaderを設定したslotを追加しmessageという名前のslot propsを追加し、defaultのslotにはmessageの他、contentの2つのslot propsを設定しています。name属性はslotを識別するためのIDなのでslot propsではないことに注意してください。
<template>
<slot name="header" message="ヘッダー"></slot>
<slot message="ユーザ" content="コンテント"></slot>
</template>
App.vueファイルで2つのtemplateタグを使ってそれぞれのslotからslot propsを取得しています。
<template>
<h1>Vue 3 入門</h1>
<User>
<template v-slot:header="slotProps">
<div>{{ slotProps }}</div>
</template>
<template v-slot:default="slotProps">
<div>{{ slotProps }}</div>
</template>
</User>
</template>
slotが複数の場合もslot propsが複数ある場合もslot propsが親コンポーネント側で取得できていることが確認できます。
slotPropsに入っているpropsがわかっている場合は分割代入を利用することができます。
<template>
<h1>Vue 3 入門</h1>
<User>
<template v-slot:header="{ message }">
<div>{{ message }}</div>
</template>
<template v-slot:default="{ message, content }">
<div>{{ message }}/{{ content }}</div>
</template>
</User>
</template>
v-slotの省略系もありv-slotを#に変更することもできます。
<template>
<h1>Vue 3 入門</h1>
<User>
<template #header="{ message }">
<div>{{ message }}</div>
</template>
<template #default="{ message, content }">
<div>{{ message }}/{{ content }}</div>
</template>
</User>
</template>
少し複雑なScoped Slot設定
シンプルなScoped Slotの利用方法は理解できたと思うので少し難しいScoped Slotの設定を行います。
子コンポーネント側で外部リソースから取得したデータをslot propsを利用して親コンポーネントに渡し親コンポーネントで表示の設定を行えることを確認します。
Userコンポーネント上でfetch関数を利用してJSONPlaceHolderを使ってユーザの一覧情報を取得します。JSONPlaceHolderは無料のサービスでhttp://jsonplaceholder.typicode.com/usersにアクセスするとユーザの一覧がJSONで戻されます。
取得したusersデータはv-forディレクティブを利用してslotタグの中で展開しslot propsのuserに設定を行い親コンポーネントに渡しています。変数を渡す場合にはv-bindディレクティブを利用する必要があるためuserに:(コロン)を設定します。
<script setup>
import { ref } from 'vue';
const users = ref([]);
const fetchPost = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
users.value = await res.json();
};
fetchPost();
</script>
<template>
<slot v-for="user in users" :user="user"></slot>
</template>
slot propsのuserを受け取った親コンポーネントではuserオブジェクトのプロパティを利用して自由に表示内容を設定することができます。下記ではnameのみ利用しています。
<template>
<h1>Vue 3 入門</h1>
<ul>
<User>
<template v-slot:default="{ user }">
<li>{{ user.name }}</li>
</template>
</User>
</ul>
</template>
ブラウザで確認するとユーザ名の一覧が表示されます。
親側で表示内容を変更できることによりあるコンポーネントでUserコンポーネントを利用してnamtとemailだけを表示させたり、別のコンポーネントではUserコンポーネントを利用してnameだけ表示させるといったことが可能になります。Userコンポーネント側で表示の設定を行なっていた場合にはそれを利用するコンポーネントはいつも同じフォーマットでしかユーザ情報を表示することができません。Userコンポーネントをより汎用的に利用することができます。
slot propsをデフォルトのslotから受け取る場合はdefaultを省略することもできます。
<template>
<h1>Vue 3 入門</h1>
<ul>
<User>
<template v-slot="{ user }">
<li>{{ user.name }}</li>
</template>
</User>
</ul>
</template>
Dynamicコンポーネント
Dynamicコンポーネントを利用することで表示するコンポーネントを動的に切り替えることができます。
ボタンを利用して画面に表示させる内容を切り替えることができる機能を実装するためにcomponentsフォルダの中に2つのファイルTokyo.vue, Kyoto.vueを作成します。
それぞれには下記の内容を記述します。
<template>
<h2>東京</h2>
<p>日本の首都です。</p>
</template>
<template>
<h2>京都</h2>
<p>日本の古都です。</p>
</template>
App.vueファイル内で2つのファイルをimportして下記のように記述します。
<script setup>
import Tokyo from './components/Tokyo.vue';
import Kyoto from './components/Kyoto.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<div>
<button>東京</button>
<button>京都</button>
</div>
<div>
<Tokyo />
<Kyoto />
</div>
</template>
切り替えの機能は実装していないので2つのボタンと2つのコンポーネントの内容がそのまま表示されます。
東京のボタンを押すと東京の内容、京都のボタンを押すと京都の内容が表示されるようにreactiveな変数とv-ifディレクティブとclickイベントを利用します。
<script setup>
import { ref } from 'vue';
import Tokyo from './components/Tokyo.vue';
import Kyoto from './components/Kyoto.vue';
const city = ref('tokyo');
</script>
<template>
<h1>Vue 3 入門</h1>
<div>
<button @click="city = 'tokyo'">東京</button>
<button @click="city = 'kyoto'">京都</button>
</div>
<div>
<Tokyo v-if="city == 'tokyo'" />
<Kyoto v-else />
</div>
</template>
設定により東京ボタンをクリックすると東京の内容が表示され、京都のボタンを押すと京都の内容が表示されます。v-ifディレクティブを利用して表示・非表示を切り替えを行なっていますがこの機能はDynamicコンポーネントの機能を利用することで変更することができます。
Dynamicコンポーネントではcomponentタグとis属性を利用します。is属性はv-bindにより変数を設定することができ、is属性の値を変更することで動的にコンポーネントを設定できるようになります。
script setupを利用した場合についてはcomponentタグとis属性を利用して下記のように設定を行うことでDynamicコンポーネントを利用することができます。
<script setup>
import { ref, computed } from 'vue';
import Tokyo from './components/Tokyo.vue';
import Kyoto from './components/Kyoto.vue';
const city = ref('tokyo');
const tabs = {
tokyo: Tokyo,
kyoto: Kyoto,
};
const tab = computed(() => tabs[city.value]);
</script>
<template>
<h1>Vue 3 入門</h1>
<div>
<button @click="city = 'tokyo'">東京</button>
<button @click="city = 'kyoto'">京都</button>
</div>
<component v-bind:is="tab"></component>
</template>
tabsとcomputedプロパティを利用することでis属性に設定するtabの情報にはコンポーネントの名前ではなくコンポーネントの情報が含まれています。
変数のcityの値をそのままisの値として設定した方法ではコンポーネントの内容がブラウザ上に表示されることはありません。デベロッパーツールで要素を見るとtokyoの場合は<tokyo></tokyo>タグが表示されています。
<template>
<h1>Vue 3 入門</h1>
<div>
<button @click="city = 'tokyo'">東京</button>
<button @click="city = 'kyoto'">京都</button>
</div>
<keep-alive> <component v-bind:is="city"></component></keep-alive>
</template>
keep-alive
Dynamicコンポーネントを利用することでボタンをクリックすることでコンポーネントを切り替えることができるようになりました。しかしis属性を利用してコンポーネントを切り替えた場合にコンポーネントの中の状態を保持することができないため各コンポーネントで状態を保持したい場合はkeep-aliveタグで囲むことで状態を保持することができます。状態を保持するとはどういうことかということを含めて動作確認を行います。
Tokyoコンポーネントにカウンターを追加します。”Add count”をクリックするとcount数が増えるだけの機能です。
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<h2>東京</h2>
<p>日本の首都です。</p>
<p>{{ count }}</p>
<button @click="count++">Add Count</button>
</template>
デフォルトではcountの値は0なのでブラウザ上には0が表示されます。
ボタンをクリックするとcount数が増えます。
次に”京都”ボタンをクリックして、再度”東京”ボタンを押してください。状態を保持できないために表示されていたcountが0になります。
count数を保持するこためにcomponentタグをkeep-aliveタグで囲みます。これで設定は完了です。
<template>
<h1>Vue 3 入門</h1>
<div>
<button @click="city = 'tokyo'">東京</button>
<button @click="city = 'kyoto'">京都</button>
</div>
<keep-alive> <component v-bind:is="tab"></component></keep-alive>
</template>
再度同じことを実施するとcount数がタブを切り替えてもcountの値が保持できることが確認できます。input要素などの入力フォームで入力した値を保持したい場合もkeep-aliveを利用することができます。
Provide/Inject
親コンポーネントから子コンポーネントにデータを渡したい時にはpropsを利用することができます。しかし親コンポーネントからデータを渡したい子コンポーネントまでの階層が深い場合はpropsを利用することは非常に手間がかかります。propsの代わりにProvide/Injectを利用することで親コンポーネントからあるコンポーネントにpropsを利用することなくデータを渡すことができます。
図で表すとProvide/Injectとpropsの違いは下記のように記述できます。propsを利用してAppコンポーネントからCompBコンポーネントにデータを渡す際はCompAを経由させる必要があります。しかしProvide/Injectを利用するとAppコンポーネントからCompAを介さずCompBに直接データを渡すことができます。
動作確認を行うためにcomponentsフォルダにCompA.vue, CompB.vueファイルを作成します。
CompA.vueファイルの中でCompBコンポーネントをimportしています。
<script setup>
import CompB from './CompB.vue';
</script>
<template>
<h2>CompAコンポーネント</h2>
<CompB />
</template>
<template>
<h3>CompBコンポーネント</h3>
</template>
App.vueファイルの中でCompAコンポーネントをimportしています。
<script setup>
import CompA from './components/CompA.vue';
</script>
<template>
<h1>Vue 3 入門</h1>
<CompA />
</template>
ブラウザ上には以下の画面が表示されます。
propsを利用した場合
Provider/Injectの動作確認する前にAppコンポーネントからCompBコンポーネントにpropsを利用してデータを渡す方法を復習しておきましょう。
messageという名前のpropsに”propsでデータ渡し”を設定してCompAに渡します。
<template>
<h1>Vue 3 入門</h1>
<CompA message="propsでデータ渡し" />
</template>
CompAでは受け取ったpropsのmessageをCompBに渡します。この場合はmessageにv-bindを設定して渡します。
<script setup>
import CompB from './CompB.vue';
const { message } = defineProps(['message']);
</script>
<template>
<h2>CompAコンポーネント</h2>
<CompB :message="message" />
</template>
CompBでは受け取ったpropsのmessageをtemplateタグで表示させています。
<script setup>
const { message } = defineProps(['message']);
</script>
<template>
<h3>CompBコンポーネント</h3>
<p>{{ message }}</p>
</template>
ブラウザ上にはpropsを介して渡されたデータ表示されます。
これがpropsを利用した方法です。
Provide/Injectを利用した場合
propsでのデータの渡し方が確認できたのでProvide/Injectを利用してAppからCompBにデータを渡します。ProvideとInjectを利用するためにはvueからprovide関数とinject関数をimportする必要があります。
App.vueファイルではprovider関数を利用してmessageに文字列”Provide/Injectでデータ渡し”を設定します。第一引数にはkey, 第二引数にはvalueを設定しています。propsとは異なり親コンポーネントのtemplateタグで何か設定を行うことはありません。
<script setup>
import CompA from './components/CompA.vue';
import { provide } from 'vue';
provide('message', 'Provide/Injectでデータ渡し');
</script>
<template>
<h1>Vue 3 入門</h1>
<CompA message="propsでデータ渡し" />
</template>
CompBコンポーネントでInject関数を利用してProvideで設定した値をkeyを利用して取得します。これでAppからComBにデータを渡すことができました。
<script setup>
import { inject } from 'vue';
const { message } = defineProps(['message']);
const message2 = inject('message');
</script>
<template>
<h3>CompBコンポーネント</h3>
<p>{{ message }}</p>
<p>{{ message2 }}</p>
</template>
ブラウザで確認するとAppのprovide関数で指定した文字列が表示されます。
Provide/Injectの設定方法だけではなくpropsの方法と違いが理解できたかと思います。
Reactiveな変数を渡す
文字列ではなくreactiveな変数を渡した場合の動作確認も行います。ref関数でmessageを定義して初期値に同じ文字列を設定します。
<script setup>
import CompA from './components/CompA.vue';
import { provide, ref } from 'vue';
const message = ref('Provide/Injectでデータ渡し');
provide('message', message);
</script>
CompBでの設定変更は必要ありません。ブラウザを確認すると先ほどと同じ結果となります。
reactiveな変数なので親コンポーネントでiput要素を利用して更新してもその更新がProvide / Injectを経由してCompBに反映されるのか確認します。
<script setup>
import CompA from './components/CompA.vue';
import { provide, ref } from 'vue';
const message = ref('Provide/Injectでデータ渡し');
provide('message', message);
</script>
input要素で文字列の更新を行うとその更新が反映されることが確認できます。
子コンポーネントで更新
Injectで渡された値を直接子コンポーネントで更新することができません。Provide/Injdectでは関数も渡すことができるのでその関数を利用して子コンポーネントから更新を行うことができます。
Appでreactiveな変数countを定義して、関数で更新できるようにaddCountを追加します。countと同様に追加したaddCountもprovide関数に設定することができます。
<script setup>
import CompA from './components/CompA.vue';
import { provide, ref } from 'vue';
const count = ref(0);
const addCount = () => {
count.value++;
};
provide('count', count);
provide('addCount', addCount);
</script>
count, addCountをinject関数で取得します。
<script setup>
import { inject } from 'vue';
const { message } = defineProps(['message']);
const count = inject('count');
const addCount = inject('addCount');
</script>
<template>
<h3>CompBコンポーネント</h3>
<p>{{ message }}</p>
<p>Count:{{ count }}</p>
<button @click="addCount">+</button>
</template>
ボタンをクリックするとCountの数がクリック毎に増えていくことが確認できます。
Provide関数で変数countと関数addCountを分けて設定しましたがオブジェクトで1つにまとめて設定を行うこともできます。
<script setup>
import CompA from './components/CompA.vue';
import { provide, ref } from 'vue';
const count = ref();
const addCount = () => {
count.value++;
};
provide('count', {
count,
addCount,
});
</script>
ComBコンポーネントではinjectを利用してcountのオブジェクトを受け取り分割代入でcount, addCountを取り出して利用します。ブラウザ上での動作は先ほどと変わりません。
<script setup>
import { inject } from 'vue';
const { message } = defineProps(['message']);
const { count, addCount } = inject('count');
</script>
reactiveを利用してデータ共有(状態管理)
コンポーネント間でデータを共有したい場合にProps, Emit先ほど説明を行なったProvide/Injectを利用しなくてもVue3ではPiniaというライブラリ(Vuexも同じ)を利用することでアプリケーション全体でデータを共有することができます。しかし、それらの機能、ライブラリを利用しなくてもreactiveな関数で定義したオブジェクトを別ファイルに分けて管理することでコンポーネント間でデータを共有することができます。
srcフォルダにstoreフォルダを作成しその下にcountStore.jsファイルを作成します。countStore.jsファイルではreactive関数を利用してcountの初期値とaddCountを設定します。設定したcounterは他のファイルでも利用できるようにexportを行う必要があります。
import { reactive } from 'vue';
export const counter = reactive({
count: 0,
addCount() {
this.count++;
},
});
countとaddCountを利用したいコンポーネントではcountStoreをimportします。ここではCompBコンポーネントでimportを行います。countはcounter.count, addCoountはcounter.addCountでアクセスすることができます。
<script setup>
import { counter } from '../store/countStore';
</script>
<template>
<h3>CompBコンポーネント</h3>
<p>Count:{{ counter.count }}</p>
<button @click="counter.addCount">+</button>
</template>
ブラウザで確認するとボタンをクリックするとカウントの数が増えていくことが確認できます。countStoreで定義したcountの値をimportしたコンポーネントから操作することができます。
さらに他のコンポーネントでもcountStore.jsをimportすることでデータ共有できているのか確認するためにComAコンポーネントではcountのみ表示させます。もちろんaddCount関数をCompAコンポーネントでも利用することができます。
<script setup>
import CompB from './CompB.vue';
import { counter } from '../store/countStore';
</script>
<template>
<h2>CompAコンポーネント</h2>
<p>Count:{{ counter.count }}</p>
<CompB />
</template>
CompBコンポーネントの”+”ボタンをクリックするとCompAとCompBコンポーネントのcountが増えることが確認できます。countStore.jsファイルで定義したreactiveな変数をデータ共有として利用することができることがわかりました。
小さなプロジェクトであればデータ共有に利用することもできますが大きなプロジェクトの場合はPiniaを利用することになります。Piniaを利用した場合のVueのDevtoolを利用してデバッグを行うことができるため開発を効率的に進めることができます。
まとめ
ここまで読み進めてもらえればコンポーネントにおけるProps, emit, Slot, Provider/Injectの基礎はかなり理解できているのではないでしょうか。Vueアプリケーションを作成するためにはさらにVue Routerや状態管理のVuexやPiniaなどの学習が必要となります。
本文書ではそれぞれの機能についても文書を公開しているのでぜひ参考にしてください。