はじめに
Vue.jsの「コンポーネント」をなんとなく理解できたので、まとめてみました。
Vue.jsの初歩的な知識はある前提です。
この辺↓が分かっていればとりあえずOKかと。
5分でわかるVue.js基礎の基礎
環境
Vue CLIを使います。
使ったことない方はインストールをお願いします。
※参考:Vue CLI スタートガイド
プロジェクトを立ち上げるとこんな感じの構成でファイルが作られていると思います。
基本的に編集していくのはApp.vueとcomponentsフォルダです。それ以外はデフォルトのまま触らなくて大丈夫です。
ターミナルで
$ npm run serve
を実行すると開発用サーバが立ちます。
その状態でhttp://localhost:8080/
にアクセスすると、下のような画面が表示されます。
vueファイルに変更を加えると、その変更内容がこのページに即座に反映されます。
試しにApp.vueの中身を全て削除すると何も表示されなくなるはずです。
変更が反映されてないなと思ったらcommand+shift+Rでスーパーリロードしましょう。
npm run serve
で開発用サーバを再起動する必要はありません。
以後、まっさらな状態からスタートしたいので、
- App.vueの中身を消去
- componentsフォルダの中にデフォルトで入っているHelloWorld.vueを削除
をやっておきましょう。
vueファイルとは
拡張子が.vue
となっているファイルです。
基本的にこのvueファイル一つで一つのコンポーネントを構成します。
vueファイルは主に
- templateタグ:コンポーネントのhtml要素を埋め込む
- scriptタグ:javascriptを記載する
- styleタグ:cssを記載する
の三部分からなります。
これらを合わせて一つの部品としてコンポーネントを作成し、それらを組み合わせて1つのWebページを作るのがVue.jsの大枠の考え方です。
<template>
<!-- ここにhtmlを記載します -->
</template>
<script>
// ここにjavascriptを記載します
</script>
<style>
/* ここにcssを記載します */
</style>
とにかくコンポーネントを使ってみる
components/Header.vue
componentsフォルダにHeader.vueを作成して以下のように記載しましょう。
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ text }}</p>
</div>
</template>
<script>
export default {
data(){
return {
title: "Header",
text: "Hello Vue.js!"
}
}
}
</script>
<style scoped>
div{
border: 1px solid blue
}
h1{
color: blue
}
p{
color:blue
}
</style>
scriptタグ内で定義した変数title``text
をtemplateタグ内で呼び出して使用しています。
また、styleタグ内で文字色と枠線を青にしています。
styleタグに併記されているscoped
は、「このコンポーネントにのみ、styleを適用しますよ」ということを明示的に宣言しています。
components/Body.vue
componentsフォルダにBody.vueを作成して以下のように記載しましょう。
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ text }}</p>
</div>
</template>
<script>
export default {
data(){
return {
title: "Body",
text: "Have a good day!"
}
}
}
</script>
<style scoped>
div{
border: 1px solid red
}
h2{
color: red
}
p{
color: red
}
</style>
やっていることはHeader.vueとほぼ一緒です。
Header.vueと区別がつきやすいよう、こちらは文字色を赤にしています。
App.vue
先ほど作成したHeader.vueとBody.vueをApp.vueから呼び出したいと思います。
以下のように記載しましょう。
<template>
<div>
<Header></Header>
<Body></Body>
<Body></Body>
</div>
</template>
<script>
import Header from './components/Header.vue'
import Body from './components/Body.vue'
export default {
components: {
Header,
Body
}
}
</script>
子コンポーネントを呼び出すにはまずscriptタグ内で子コンポーネントのvueファイルをインポートし、components要素に使用するコンポーネント名を記載します。
あとはtemplateタグ内で、子コンポーネント名をそのままタグとして記載してあげれば、その場所に子コンポーネントが挿入されます。
今回の例だとこのような画面になります。
一番上にHeaderコンポーネント、その後二つ続けてBodyコンポーネントが配置されます。
コンポーネント間のデータ受け渡し
親→子
親側のデータ渡し口(タグ属性)
データ渡し口は、子コンポーネントを呼び出しているタグ内でカスタム属性を定義するだけです。
例えばこんな感じで、username
属性を定義することで、子コンポーネント側でusername
という変数で受け取ることができます。
<template>
<div>
<Header :username='name'></Header>
〜略〜
</div>
</template>
<script>
〜略〜
export default {
data(){
return {
name: "kiyokiyo"
}
},
〜略〜
}
</script>
子側のデータ受け取り口(props)
子コンポーネント側ではprops
を使ってデータを受け取ります。
親コンポーネント側で定義した属性名とデータ型をオブジェクト型で記載します。
<script>
export default {
props: {
username: String
},
〜略〜
}
</script>
受け取ったデータはtemplateタグ内で自由に使うことができます。
<template>
<div>
〜略〜
<p>Welcome! {{ username }}!</p>
</div>
</template>
こんな感じでApp.vueで定義した変数を、Headerコンポーネントに渡して表示させることができました。
子→親
Bodyコンポーネントでそれぞれボタンクリックでインクリメントされた値をAppコンポーネントに送って、トータルを計算して表示させたいと思います。
Bodyコンポーネントでのカウントアップの機構と、Appコンポーネントでのトータル表示の機構だけ先に作りました。データを受け渡すための機構をまだ用意していないので、この段階だとうまく動きません。
<template>
<div>
<Header :username='name'></Header>
<Body></Body>
<Body></Body>
<p>total : {{ totalcount }} </p>
</div>
</template>
<script>
import Header from './components/Header.vue'
import Body from './components/Body.vue'
export default {
data(){
return {
name: "kiyokiyo",
count1: 0,
count2: 0,
totalcount: 0
}
},
components: {
Header,
Body
}
}
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<p><button @click="increment">+1</button> {{ count }} </p>
</div>
</template>
<script>
export default {
data(){
return {
title: "Body",
text: "Have a good day!",
count: 0
}
},
methods: {
increment(){
this.count += 1;
}
}
}
</script>
子側のデータ渡し口($emit)
this.$emit
でカスタムイベントを定義してあげます。ここではaddイベントを定義して、その引数としてカウントの値を渡しています。
〜略〜
<script>
export default {
〜略〜
methods: {
increment(){
〜略〜
this.$emit("add",this.count);
}
}
}
</script>
親側のデータ受け取り口(イベントと関数引数)
子コンポーネントで定義したaddイベントは、親コンポーネント側のタグ属性で@add=[関数名]
と記載することで、methodsで定義してある関数に繋げることができます。
その関数で引数を定義してあげると、子コンポーネントからの値を受け取ることができます。
今回の例では、引数として受け取ったcount
をそれぞれcount1
もしくはcount2
に格納し、それらを足してあげることでtotalcount
を更新しています。
<template>
<div>
〜略〜
<Body @add="add1"></Body>
<Body @add="add2"></Body>
<p>total : {{ totalcount }} </p>
</div>
</template>
<script>
〜略〜
export default {
〜略〜
methods:{
add1(count){
this.count1 = count;
this.totalcount = this.count1 + this.count2;
},
add2(count){
this.count2 = count;
this.totalcount = this.count1 + this.count2;
}
}
}
</script>
html要素を親から子へ渡す
親側のデータ渡し口(template v-slot)
子コンポーネント名のタグの中にtemplateタグを置いて、その中にhtml要素を書くことで渡すことができます。
v-slot:[スロット名]
で、スロット名を指定します。ここで指定した名前を子コンポーネント側で使用します。
今回の例では1つのtemplateしか使用していませんが、スロット名を変えれば複数のtemplateを子コンポーネントに送ることもできます。
結果が分かりやすいよう、styleタグで文字色が緑になるようにしています。
<template>
<div>
<Header :username='name'>
<template v-slot:message>
<p>Let's enjoy programming!</p>
</template>
</Header>
〜略〜
</div>
</template>
〜略〜
<style scoped>
p{
color: green
}
</style>
子側のデータ受け取り口(slot)
子コンポーネント側ではslotタグを使用してhtml要素を受け取ります。
この際に、親コンポーネントで指定したスロット名をname属性として記載することで、送られてくるtemplateとその挿入箇所を対応させることができます。
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ text }}</p>
<slot name="message"></slot>
<p>Welcome! {{ username }}!</p>
</div>
</template>
こんな感じで、親コンポーネントで定義されたhtml要素を子コンポーネントに渡すことができました。
あくまで親コンポーネントで定義された要素なので、Headerコンポーネントに渡されているものの、文字色が緑色のままになっていることがわかると思います。
コンポーネント間で双方向データバインディングをやる
子側のinputで入力された値を受け取って親側のコンポーネントで表示させたいと思います。
子側のデータ渡し口($emit)
$emitで、inputに何かしら入力があるたびに親のinputイベントを発火させています。その際にinputのvalue値を引数として送りこんでいます。このやり方は子→親のデータ受け渡しと同じです。
また、親コンポーネントからvalueを受け取れるよう、propsにvalueを宣言しています。
なお、コンポーネント間の双方向データバインディングの場合、$emitの中身のイベント名input
と、propsで受け取る変数名value
は必ずこの名前にしないといけないので注意です。
<template>
<div>
〜略〜
<label for="condition">How are you?</label>
<input id="condition" type="text" :value="value" @input="$emit('input',$event.target.value)">
</div>
</template>
<script>
export default {
props: {
〜略〜
value: String
},
〜略〜
}
</script>
親側のデータ受け取り口(v-model)
v-modelで入力を受け取る変数を指定しています。
v-modelは内部的にinputイベント(@ivent
)を保持しており、inputイベントが発火するたびに引数として渡されたデータ(今回で言うと$event.target.value)で、指定した変数(今回で言うとInputData.condition)を更新するようになっています。
また、v-modelはv-bind:value
を内部的に保持しているため、指定した変数が親コンポーネント側の処理で書き換わった場合、その内容が即時に子コンポーネントにも反映されるようになります。
子コンポーネントで必ずinput
及びvalue
を使わないといけなかったのは、親コンポーネントのv-modelで内部保持している名称に合わせる必要があったからです。
<template>
<div>
<Header :username='name' v-model="InputData.condition">
〜略〜
</Header>
〜略〜
<p>condition : {{ InputData.condition }} </p>
</div>
</template>
<script>
〜略〜
export default {
data(){
return {
〜略〜
InputData: {
condition: ""
}
}
},
〜略〜
}
</script>
コンポーネントを動的に切り替える
componentタグを利用して、属性にis=[コンポーネント名]
を記載することで、使用するコンポーネントを指定することができます。
それを利用して、componentNameにバインドして、ボタンクリックによりcomponentNameを動的に変更することによって、表示するコンポーネントも動的に切り替わる、という仕組みを実現することができます。
<template>
<div>
〜略〜
<button @click="componentName = 'Header'">Header</button>
<button @click="componentName = 'Body'">Body</button>
<component :is="componentName"></component>
</div>
</template>
<script>
〜略〜
export default {
data(){
return {
〜略〜
componentName: "Header"
}
},
〜略〜
}
</script>
ただ、この書き方だとボタンで切り替えるたびにコンポーネントが初期化されるため、inputに入力した文字や、インクリメントした数字などもリセットされます。
初期化されてしまうと問題がある場合はkeep-aliveタグで囲ってあげることで、コンポーネントの状態を保持したまま切り替えることができます。
<template>
<div>
〜略〜
<keep-alive>
<component :is="componentName"></component>
</keep-alive>
</div>
</template>
おわりに
- templateタグ:htmlを書く
- scriptタグ:javascriptを書く
- styleタグ:cssを書く
を合わせて一つの部品としてコンポーネントを作成し、それらを組み合わせて1つのWebページを作る、という思想が直感的でとっつきやすいなと思いました。
コンポーネントの基礎は抑えられたので、あとは実際にプロダクトを作りながら理解を深めていきたいと思います。