vue.jsコンポーネントの基礎
Vue.jsでのコンポーネントはVue.jsの中核をなす重要な機能です。コンポーネント間のデータの受け渡しや記述方法など最初は難しさを感じるかと思います。ぜひ本文書を参考にコンポーネントの理解を深めてください。
本文書はVueのバージョン2でCDNを利用して動作確認を行っています。Vue 2はEOL(End of Life)になっているので新たに学習を開始する人はVue3で始めることになります。Vue 3のComposition APIのsetup scriptを利用した場合の記事を別で公開しているので参考にしてください。バージョンがアップして記述方法が変わってもコンポーネントの基本的な箇所は変わっていないためVue2の知識はVue3で活用することができます。
コンポーネントとは
企業HPやブログのサイトを見るとヘッダー、フッター、メイン、サイドバーというようにいくつかのパーツに別れて構成されています。特にヘッダーやフッターなどは同じ内容を表示する場合が多くすべてのページで何度も何度も同じ内容を記述することは非効率です。そのためヘッダー用のパーツ、フッター用のパーツを個別に作成し、すべてのページからパーツを読み込んで共有化することが一般的です。可能なかぎり構成要素をパーツ化し共有化することができれば開発、メンテナンスの効率が格段に上がります。Vue.jsではコンポーネントという機能を使ってパーツ化し再利用することで効率よく開発、メンテナンスを行うことができます。
コンポーネントの設定
コンポーネントの作成
コンポーネントを利用するためには、コンポーネントを定義して登録する必要があります。
Vue.component(コンポーネントの名前,{
template : 'HTMLに表示させたい内容'
})
実際に使用する場合は下記のように記述します。hello-worldという名前のコンポーネントを作成しその中身にはh1タグで囲まれたHello Worldという文字列を持つ要素が含まれていることを意味します。
Vue.component('hello-world',{
template : '<h1>Hello World</h1>'
})
コンポーネントの登録を行うとVueインスタンス下にあるhtml内から直接コンポーネントを利用することができます。利用する場合は、コンポーネント作成時に設定した名前のタグ名hello-worldを使ってhtmlに組み込みます。
<div id="app">
<hello-world></hello-world>
</div>
ブラウザで見ると登録したコンポーネントのtemplateプロパティのh1タグの内容が表示されることが確認できます。
Vueインスタンスを含めたコード全体は下記のようになります。cdnを使ってvue.jsを読み込んでいます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Component</title>
</head>
<body>
<div id="app">
<hello-world></hello-world>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('hello-world',{
template : '<h1>Hello World</h1>'
})
var app = new Vue({
el: '#app',
data: {
}
})
</script>
</body>
</html>
Vue.componentの定義は、new Vueよりも前に記述する必要があります。new Vueよりも後に作成した場合は、Unknown custom element: <hello-world> – did you register the component correctly?のメッセージがコンソールに表示され、ブラウザには何も表示されません。エラーメッセージを日本語にすると”コンポーネントは正しく登録されましたか?”という内容です。
コンポーネントの再利用
コンポーネントは再利用することができ、hello-worldのタグを複数記述するだけで同じ内容を表示させることが可能です。コンポーネントを一度作成すると部品としてどこからでも再利用することができます。
<div id="app">
<hello-world></hello-world>
<hello-world></hello-world>
<hello-world></hello-world>
</div>
ローカルへの登録方法
先ほどはVue.componentを利用してコンポーネントの登録を行いました。コンポーネントの登録には、グローバルとローカルの2つの方法があります。Vue.componentを使う場合はグローバルへの登録となり、別のコンポーネントで利用することができます。一方ある特定のコンポーネントだけで利用したい場合は、ローカルで登録を行い利用することができます。ローカルへの登録は、Vueインスタンスにcomponentsプロパティを追加することで行います。
var app = new Vue({
el: '#app',
data: {
},
components: {
'hello-world': {
template: '<h1>Hello World</h1>'
}
}
})
別ファイルからimport
コンポーネントは別ファイルとして分けることができます。通常はcomponentsフォルダの下などにファイルを作成しますがここではindex.htmlファイルと同じフォルダ内にHelloWorld.jsファイルを作成します。index.htmlファイル内でimportを行うのでexport defaultでオブジェクトとしてexportしています。
export default {
template: '<h1>Hello World</h1>',
};
index.htmlファイルでimportによりHelloWorldコンポーネントとして読み込みます。ローカル登録の場合は下記のように行います。
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Component</title>
</head>
<body>
<div id="app">
<hello-world></hello-world>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="module">
import HelloWorld from "./HelloWorld.js"
const app = new Vue({
el: '#app',
data: {
},
components: {
'hello-world': HelloWorld
}
})
</script>
</body>
</html>
export, importを利用する場合はscriptタグにtype=”module”を設定する必要があります。もし設定していない場合には”Uncaught SyntaxError: Cannot use import statement outside a module”のエラーが発生します。
グローバル登録の場合は下記のようにVue.componentを利用して記述することができます。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="module">
import HelloWorld from './HelloWorld.js';
Vue.component('hello-world', HelloWorld);
const app = new Vue({
el: '#app',
data: {},
});
</script>
グローバルとローカル登録の違い
Vue.componentを使ったグローバル登録とVueインスタンスの中で行うローカル登録の違いは下記の例を使って理解することができます。
1つのページに2つのVueインスタンスを作成し、hello-worldのコンポーネントはグローバル登録、hello-viewのコンポーネントは2つめのapp2インスタンスのみに登録します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Component</title>
</head>
<body>
<div id="app">
<hello-world></hello-world>
<hello-view></hello-view>
</div>
<div id="app2">
<hello-world></hello-world>
<hello-view></hello-view>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('hello-world',{
template : '<h1>Hello World</h1>'
})
var app = new Vue({
el: '#app',
})
var app2 = new Vue({
el: '#app2',
components: {
'hello-view': {
template: '<h1>Hello View</h1>'
}
}
})
</script>
</body>
</html>
下記のように1つ目のappのVueインスタンスではapp2にローカル登録したhello-viewコンポーネントが使用できないためHello Viewの文字列が表示されませんが、ローカル登録したapp2のVueインスタンス側には表示されます。
コンソールを見ると”Unknown custom element: – did you register the component correctly? (不明のカスタムエレメント、正しくコンポーネントを登録しましたか?)”のエラーが表示されます。
templateに複数のタグ
Vue 3ではこの制限はなくなっています。
templateに複数のタグを入れる場合に注意点があるので確認しておきます。以下のようにtemplateタグの中にpタグを2つ挿入します。
Vue.component('hello-world',{
template: '<p>{{ message }}</p><p>Hello World</p>',
data: function(){
return {
message : 'Hello Vue'
}
}
})
ブラウザで確認すると2つ目のpタグのHello Worldが画面には表示されません。コンソールのログには、”Component template should contain exactly one root element.” とエラーメッセージが表示されます。エラーの内容は”コンポーネントテンプレートは1つのルート要素を持たなければならない”というエラーです。上記ではpタグが2つ挿入されているため、1つのルート要素にはなっていません(2つのルート要素になっている)。そのため、この2つのpタグを含む上位のタグをつける必要があります。ここではdivタグをつけます。divをつけることでdivが1つのルート要素になり、2つのpタグはdivの子要素となりエラーメッセージも消えてブラウザにはHello Vue Hello Worldが表示されます。
Vue.component('hello-world',{
template: '<div><p>{{ message }}</p><p>Hello World</p></div>',
data: function(){
return {
message : 'Hello Vue'
}
}
})
コンポーネントでdataを使う
templateを追加してh1タグでブラウザに表示を行いました。コンポーネントでは、templateにhtmlタグを挿入するだけではなくdataプロパティも利用することができます。dataの設定方法はVueインスンス作成時と同じですが、dataは関数にする必要があります。
Vue.component('hello-world',{
template: '<h1>{{ message }}</h1>',
data: function(){
return {
message : 'Hello Vue'
}
}
})
複数のデータプロパティを使いたい場合は下記のようになります。
data: function(){
return {
message : 'Hello Vue',
message2 : 'Hello World'
}
}
以下はVueインスタンスの場合のdataの記述方法です。違いをしっかりと覚えておいて記述間違いをしないように注意しましょう。
data: {
message: 'Hello Vue',
message2: 'Hello World',
}
メソッドを追加する
コンポーネントはdataだけではなくメソッドも使うことができます。メソッドの追加はVueインスタンス作成時と同じ方法でmethodsプロパティで行います。
例を使ってコンポーネントのメソッドを理解していきます。コンポーネントの名前をbutton-counterとして、dataにcount変数を持たせ、countUp、countDownの2つのメソッドを追加します。ボタンにはv-onディレクティブでclickイベントを設定して、Upボタンを押すとcountが1増え、Downボタンを押すとcountが1減るプログラムを作成します。
Vue.component('button-counter',{
template: '<p>カウント:{{ count }} <button v-on:click="countUp">Up</button><button v-on:click="countDown">Down</button></p>',
data: function(){
return {
count : 0
}
},
methods: {
countUp: function(){
this.count++
},
countDown: function(){
this.count--
},
}
})
htmlにはbutton-counterタグを追加します。
<div id="app">
<button-counter></button-counter>
</div>
Upボタンを押すとカウントが増え、Downボタンを押すとカウントが減ります。コンポーネントを使ってカウンターを作成することができました。
コンポーネントは再利用できることを先ほど学んだので、button-counterタグを複数設定してみましょう。
<div id="app">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
コンポーネントを複数作成した場合それぞれが独立したものとして作成されるので、Up、Downボタンはそのコンポーネント内のbutton-counterのみ影響を与えることができるため、countの値もそれぞれのコンポーネントで別々の値を保持することできます。
template内のhtmlを改行する
下記のようにtemplateに指定するタグが長い場合に途中で改行したい場合があります。シングルクオテーションで囲まれている場合は途中で改行を行うと正常に動作しません。
Vue.component('button-counter',{
template: '<p>カウント:{{ count }} <button v-on:click="countUp">Up</button><button v-on:click="countDown">Down</button></p>',
templateで設定するhtmlが長い場合は、`(バッククォート/バックティック)を使用してください。下記は意図的にタグごとに改行していますが、改行を行なってもプログラムは正常に動作します。この記述はテンプレートリテラルと呼ばれます。Vueの機能ではなくJavaScriptの機能です。
Vue.component('button-counter',{
template: `<p>
カウント:{{ count }}
<button v-on:click="countUp">
Up
</button>
<button v-on:click="countDown">
Down
</button>
</p>`,
データの受け渡し(親→子) Propsの理解
ここまでの説明でコンポーネント単独での利用方法がわかりました。次はコンポーネント間でのデータの受け渡しの説明を行います。コンポーネント間のデータの受け渡しができれば、別々に作成されたコンポーネントを連携して利用することができます。
親コンポーネントから子コンポーネント、子コンポーネントから親コンポーネントと異なる方法でデータの受け渡しを行うので、混乱しないように注意する必要があります。
親と子について
本文書の最初に使ったHello Worldの例では、親コンポーネントは、hello-worldのコンポーネントを利用したVueインスタンスです。子コンポーネントはVue.componentで作成したhello-worldに対応します。
ここでは行いませんが子コンポーネント(hello-world)のtemplateプロパティでは、別のコンポーネントを利用することができます。その場合はhello-worldが親のコンポーネントになり、利用された別のコンポーネントが子コンポーネントになります。アプリケーションが大きくなるのとコンポーネントの階層構造は深くなることになります。
親から子へのデータの受け渡し
親コンポーネントから子コンポーネントへのデータの受け渡しには、propsを使用します。
Vue.component('hello-world',{
template: '<h1>Hello World and {{ message }}</h1>',
props: ['message']
})
親コンポーネントはmessage属性としてその値を渡します。
<div id="app">
<hello-world message="Hello Vue"></hello-world>
</div>
ブラウザで確認するとmessage属性の値が親コンポーネントから子コンポーネントに渡され表示されることが確認できます。
全体のコードです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Component</title>
</head>
<body>
<div id="app">
<hello-world message="Hello Vue"></hello-world>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('hello-world', {
template: '<h1>Hello World and {{ message }}</h1>',
props: ['message'],
});
var app = new Vue({
el: '#app',
data: {},
});
</script>
</body>
</html>
親のdataを子へ
v-bindを使うことで親コンポーネントで設定したdataプロパティを子コンポーネントに渡すことができます。
inputTextを親コンポーネントのdataに追加します。
var app = new Vue({
el: '#app',
data: {
inputText: ''
}
})
Vue.componentは先ほど同様にmessage属性を使って親からデータを受け取ります。
Vue.component('hello-world',{
template: '<h1>Hello World and {{ message }}</h1>',
props: ['message']
})
message属性をv-bindし、その値はdataプロパティのinputTextを設定します。これだけでinputTextの値は子コンポーネントに渡されますが親で変更されたinputTextが即座に反映されるのか確認するためにinput要素を追加して、v-modelでinputTextをバイディングします。
<div id="app">
<input type="text" v-model="inputText">
<hello-world v-bind:message="inputText"></hello-world>
</div>
フォームに入力した内容が即座にブラウザに反映されることが確認できます。
リストデータを子に渡す
v-forディレクティブを利用して、リストデータを子コンポーネントに渡すこともできます。dataにリストデータの追加を行います。
var app = new Vue({
el: '#app',
data: {
posts: [
{'id':0, 'title': 'vue.jsの基礎', 'content': 'about vue.js...'},
{'id':1, 'title': 'componentの基礎', 'content': 'about component...'},
{'id':2, 'title': 'Vue CLIの基礎', 'content': 'about Vue CLIの基礎...'},
]
}
})
v-forを使用し、値を渡す時にpostオブジェクトとして渡すことができます。
<div id="app">
<blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></blog-post>
</div>
keyを設定する理由は知りたい人は下記が参考になります。
子コンポーネントはpostをオブジェクトとして受け取り、templateの中でpostの展開を行います。
Vue.component('blog-post',{
template: '<div><h2>{{ post.title }}</h2><p>{{ post.content }}</p></div>',
props: ['post']
})
ブラウザでもpost毎にリストが展開されて表示されることを確認することができます。
先程はpostオブジェクトを渡して子コンポーネントで展開しましたが、オブジェクトではなく変数として渡すこともできます。
<div id="app">
<blog-post v-for="post in posts" v-bind:key="post.id" v-bind:title="post.title" v-bind:content="post.content"></blog-post>
</div>
dataを受け取る側の子コンポーネントのpropsで受け取るそれぞれの属性名を設定します。
Vue.component('blog-post',{
template: '<div><h2>{{ title }}</h2><p>{{ content }}</p></div>',
props: ['title','content']
})
データの受け渡し(子→親) Emitの理解
親から子へデータを渡したい場合はv-bindとpropsを使いました。子から親にデータを渡したい場合は、$emitというメソッドとイベントを利用して行います。propsでのデータ渡しに比較して複雑で最初は難しいと感じるかもしれませんが慣れていきましょう。
読み進めるためには、v-onディレクトリを使ったイベントの処理は理解しておく必要があります。
$emitを使って子から親へ
子から親へのデータの受け渡しは少し複雑ですが、一度自分の手を動かして設定を行えば比較的楽に理解することができます。
$emitを使った子コンポーネントから親コンポーネントへのデータの受け渡しの一連です。
- 子コンポーネント側でボタンに対してクリックイベントを設定します。
- クリックイベントが発生したら、子コンポーネントはそのイベントの処理(clickEvent)で$emitメソッドを実行し、親に渡したいイベント(from-child)を発生させます
- 親は子からくるイベントに対するalertMessageメソッドを設定しfrom-childイベントを待ちます。
- 親は子が発生させたイベント(from-child)を受け取ってalertMessageメソッドを実行します。
1.子コンポーネント側でボタンに対してクリックイベントを設定します
Vue.component('emit-event',{
template: '<button v-on:click="clickEvent">ボタン</button>',
2.クリックイベントが発生したら、子コンポーネントはそのイベントの処理(clickEvent)で$emitメソッドを実行し、親に渡したいイベント(from-child)を発生させます
methods: {
clickEvent: function(){
this.$emit('from-child')
}
}
3.親は子からくるイベントに対するalertMessageメソッドを設定しfrom-childイベントを待ちます。
<div id="app">
<emit-event v-on:from-child="alertMessage"></emit-event>
</div>
4.親は子が発生させたイベント(from-child)を受け取ってalertMessageメソッドを実行します。
var app = new Vue({
el: '#app',
methods:{
alertMessage: function(){
alert('子からイベント受け取ったよ')
}
}
})
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Component Emit</title>
</head>
<body>
<div id="app">
<emit-event v-on:from-child="alertMessage"></emit-event>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('emit-event',{
template: '<button v-on:click="clickEvent">ボタン</button>',
methods: {
clickEvent: function(){
this.$emit('from-child')
}
}
})
var app = new Vue({
el: '#app',
methods:{
alertMessage: function(){
alert('子からイベント受け取ったよ')
}
}
})
</script>
</body>
</html>
子コンポーネントでv-on:clickの値にそのまま$emitを記述することもできます。直接記述をすることでclickEventメソッドも必要ではなくなります。
template: `<button v-on:click="$emit('from-child')">ボタン</button>`,
子から親へデータを送る
先程の例では、子で発生させたイベントを使って親に伝えることはできました。次は、子コンポーネント側のデータを親側に送ってみましょう。
子コンポーネントに入力フォームを作成し、入力後送信ボタンを押すとイベントが発生し、$emitメソッドを利用して、入力した内容を親に送ります。
Vue.component('emit-event',{
template: `<div>
<input type="text" v-model="inputText">
<button v-on:click="clickEvent">送信ボタン</button>
</div>`,
data: function(){
return {
inputText : ''
}
},
methods: {
clickEvent: function(){
this.$emit('from-child',this.inputText)
}
},
})
$emitは引数を取ることができ、この引数を利用してデータを送ります。複数のデータを渡す場合はオブジェクトを利用して渡すことができます。
this.$emit('from-child',this.inputText,XXX)
親側で子からのイベントの設定を行います。
<div id="app">
<p>{{ message }}</p>
<emit-event v-on:from-child="receiveMessage"></emit-event>
</div>
receiveMessageメソッドは受け取ったmessageをdataのmessageに設定します。
var app = new Vue({
el: '#app',
data: {
message: ''
},
methods:{
receiveMessage: function(message){
this.message = message;
}
}
})
子コンポーネントのフォームに入力したテキストが親コンポーネントに渡され、ブラウザに表示することを確認できます。
コンポーネントとSlot(スロット)
slotの機能を利用すると親側でコンポーネントタグの間に挿入した内容を子コンポーネント側で表示することができます。
Slotについてはvue.js2.6+で記述方法に変更もありました。Slotについては下記の文書を読むことをおすすめします。
slotによる挿入
親側で下記のように子コンポーネントタグの間にコンテンツを挿入します。
<div id="app">
<slot-test>スロットによる挿入</slot-test>
</div>
子側のコンポーネントでは親側で挿入されたコンテンツを表示させる場所にslotタグを入れておきます。
Vue.component('slot-test',{
template: '<p><slot></slot></p>',
}),
ブラウザで確認すると親側で子コンポーネントタグの間に挿入したコンテンツが下記のように表示されます。
slotの初期値の設定
slotタグの間に初期値を設定することで親側からのSlotの挿入がない場合は設定した初期値を表示させることができます。
Vue.component('slot-test',{
template: '<p><slot>デフォルト</slot></p>',
})
親側で挿入するコンテンツがあるものとないもの2つのコンポーネントタグを記述します。
<div id="app">
<slot-test>スロットによる挿入</slot-test>
<slot-test></slot-test>
</div>
挿入がない場合は、初期値であるデフォルトが表示されることが確認できます。
slotに名前をつける
先程まではslotに挿入できる内容は1つでしたが、複数ある場合は表示させる場所を識別するためslotに名前をつけることができます。slotタグにname属性をつけその値にslot名を設定することで識別します。
Vue.component('slot-test',{
template: `<div>
<h1><slot name="title">タイトル</slot></h1>
<p><slot name="content">内容</slot></p>
</div>`,
})
slotを挿入する親側ではタグにslot属性を追加しその値に子コンポーネントで定義されているslotの名前を入力してその間に渡したいコンテンツを挿入します。
<div id="app">
<slot-test>
<div slot="title">vue.jsの基礎</div>
<p slot="content">vue.jsについて</p>
</slot-test>
</div>
ブラウザで確認すると指定したslot名の通りにコンテンツが表示されることが確認できます。
HTMLのソースを確認するとslot属性を設定したdivタグとpタグも一緒に表示されます。
div, pタグを表示させないため、templateタグに変更を行います。templateタグを利用すると先ほどまでコンソールに表示されていたdivタグとpタグが表示されなくなり期待通りのタグ設定になります。
<div id="app">
<slot-test>
<template slot="title">vue.jsの基礎</template>
<template slot="content">vue.jsについて</template>
</slot-test>
</div>