VueコンポーネントはVueの中核をなす重要な機能です。コンポーネント間のデータの受け渡しや記述方法など最初は難しさを感じるかと思います。ぜひ本文書を参考にコンポーネントの理解を深めてください。

コンポーネントとは

企業HPやブログのサイトを見るとヘッダー、フッター、メイン、サイドバーというようにいくつかのパーツに別れて構成されています。特にヘッダーやフッターなどはどのページでも同じ内容の場合が多く1つのデータを共有する仕組み使って構築するのが一般的です。このようにパーツ化して共有化することで開発、メンテナンスの効率が格段に上がります。vue.jsではコンポーネントという機能を使って部品化し再利用することで効率よく、開発、メンテナンスを行うことができます。

コンポーネントの設定

コンポーネントの作成

コンポーネントを利用するためには、コンポーネントを定義して登録する必要があります。


Vue.component(コンポーネントの名前,{
  template : 'HTMLに表示させたい内容'
})	
templateプロパティ以外にもVueインスタンスと同様にdata, computed, methods等のプロパティを持つことができます。記述方法や使い方は、これから順次説明を行っていきます。

実際に使用できるようにするためには、下記のように記述します。


Vue.component('hello-world',{
  template : '<h1>Hello World</h1>'
})	

コンポーネントの登録を行うとVueインスタンス下にあるhtml内から直接利用することができます。利用する場合は、コンポーネント作成時に設定した名前のタグを使ってhtmlに組み込みます。

Vueインスタンス下というのは、Vueインスタンス作成時のnew Vueのelで指定しているidタグの中の要素のことを意味します。本文書では#appです。

<div id="app">
	<hello-world></hello-world>
</div>
コンポーネントの名前は任意のため、その中に記述する内容を要約した名前をつけることでメンテナンスも楽になります。

ブラウザで見ると登録したコンポーネントのtemplateプロパティのh1タグの内容が表示されることが確認できます。

コンポーネントでHello World
コンポーネントでHello World

Vueインスタンスを含めたコード全体は下記のようになります。


<!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?のメッセージがコンソールに表示され、ブラウザには何も表示されません。メッセージは、コンポーネントは正しく登録されましたか?という内容です。

本文書では、Vueが設定通りに表示されない場合はコンソール(console)を使ってエラーを確認します。上記のUnknown custom elementもコンソールを使って確認しています。Chromeの場合、開発ツール(デベロッパーツール)から確認することができます。

コンポーネントの再利用

コンポーネントは再利用することができます。hello-worldのタグを複数記述するだけで同じ内容を表示させることが可能です。コンポーネントを一度作成すると部品としてどこからでも利用することができます。


<div id="app">
	<hello-world></hello-world>
	<hello-world></hello-world>
	<hello-world></hello-world>
</div>
コンポーネントの再利用
コンポーネントの再利用

ローカルへの登録方法

先ほどはVue.componentを利用してコンポーネントの登録を行いました。コンポーネントの登録には、グローバルとローカルがあります。Vue.componentを使う場合はグローバルへの登録となり、別のコンポーネントで利用することができます。一方ある特定のコンポーネントだけで利用したい場合にローカルに登録し利用することができます。ローカルへの登録は、Vueインスタンスにcomponentsプロパティの追加をすることで行います。


var app = new Vue({
  el: '#app',
  data: {
  },
  components: {
    'hello-world': {
      template: '<h1>Hello World</h1>'
    }
  }
})	

templateに複数のタグ

templateに複数のタグを入れる場合に注意点があるので確認しておきます。以下のようにtemplateの中にpタグを2つ挿入します。


Vue.component('hello-world',{
  template: '<p>{{ message }}</p><p>Hello World</>',
  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'
    }
  }
})	
1つのルートタグ忘れはよく発生しがちなミスなのでこのルールはしっかりと記憶しておきましょう。

コンポーネントでdataを使う

templateを追加してh1タグでブラウザに表示を行いました。コンポーネントでは、templateにhtmlタグを挿入するだけではなくdataも使用することができます。dataの設定方法はVueインスンス作成時と同じですが、dataは関数にする必要があります。


Vue.component('hello-world',{
  template: '<h1>{{ message }}</h1>',
  data: function(){
    return {
      message : 'Hello Vue'
    }
  }
})	

複数のdataを使いたい場合は下記のようになります。


  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の値もそれぞれのコンポーネントで別々の値を保持することできます。

複数のカウンターを表示
複数のカウンターを表示

データの受け渡し(親→子)

ここまでの説明でコンポーネント単独での利用方法がわかりました。次はコンポーネント間でのデータの受け渡しの説明を行います。コンポーネント間のデータの受け渡しができれば、別々に作成されたコンポーネントも連携して利用することができます。

親コンポーネントから子コンポーネント、子コンポーネントから親コンポーネントと異なる方法でデータの受け渡しを行うので、混乱しないように注意する必要があります。

親と子について

本文書の最初に使った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']
})	
ここでのデータはVueインスタンス内にあるdataプロパティのデータではありません。後ほど説明しますが、dataプロパティの場合はv-bindを利用して渡します。[/comennt]

親コンポーネントはmessage属性としてその値を渡します。


<div id="app">
  <hello-world message="Hello Vue"></hello-world>
</div>

ブラウザで確認するとmessage属性の値が親コンポーネントから子コンポーネントに渡され表示することが確認できます。

propsを使った処理
propsを使った処理

全体のコードです。


<!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-bind, v-modelを使用したコンポーネント
v-bind, v-modelを使用したコンポーネント

リストデータを子に渡す

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>

[comment]v-forでv-bind:keyを使用せずに実行するとコンソールにはcomponent lists rendered with v-for should have explicit keysのメッセージが表示されます。v-forではkeysを設定して実行することを進めるメッセージです。ブラウザ側にはエラーが表示されませんが、コンソールにはメッセージがでているのでkeyを設定するようにしてください。

子コンポーネントはpostをオブジェクトとして受け取り、templateの中でpostの展開を行います。


Vue.component('blog-post',{
  template: '<div><h2>{{ post.title }}</h2><p>{{ post.content }}</p></div>',
  props: ['post']
})	

ブラウザでもpost毎にリストが展開されて表示されることを確認することができます。

v-forを使ってリスト表示
v-forを使ってリスト表示

先程は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']
})	

データの受け渡し(子→親)

親から子へdataを渡したい場合はv-bindとpropsを使いました。子から親にdataを渡したい場合は、$emitというメソッドとイベントを利用して行います。propsでのデータ渡しに比較して複雑で最初は難しいと感じるかもしれませんが慣れていきましょう。

読み進めるためには、v-onディレクトリを使ったイベントの処理は理解しておく必要があります。

$emitを使って子から親へ

子から親へのデータの受け渡しは少し複雑ですが、一度自分の手を動かして設定を行えば比較的楽に理解することができます。

$emitを使った子コンポーネントから親コンポーネントへのデータの受け渡しの流れです。

  1. 子コンポーネント側でボタンに対してクリックイベントを設定します。
  2. クリックイベントが発生したら、子コンポーネントはそのイベントの処理(clickEvent)で$emitメソッドを実行し、親に渡したいイベント(from-child)を発生させます
  3. 親は子からくるイベントに対するalertMessageメソッドを設定しfrom-childイベントを待ちます。
  4. 親は子が発生させたイベント(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>`,

子から親へdataを送る

先程の例では、子で発生させたイベントを使って親に伝えることはできました。次は、子コンポーネント側のdataを親側に送ってみましょう。

子コンポーネントに入力フォームを作成し、入力後送信ボタンを押すとイベントが発生し、$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)
    }
  },
})	
templateの文字が長くなったので改行を行っています。1行の場合はシングルクオート(‘)でしたが、複数の場合は、バッククオート(`)を使います。バッククオートはキーボードの@の場所にあります。

$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.componentでは、親側でコンポーネントタグの間に挿入された内容を表示する場所にslotタグを入れます。


Vue.component('slot-test',{
  template: '<p><slot></slot></p>',
}),

親側では下記のようにコンポーネントタグの間に挿入します。


<div id="app">
  <slot-test>スロットによる挿入</slot-test>
</div>
slotを使用して表示
slotを使用して表示

slotの初期値の設定

親側から挿入がない場合にも表示させるためにはslotタグの間に初期値を設定しておきます。


Vue.component('slot-test',{
  template: '

デフォルト

', })

親側で挿入があるものとないもののコンポーネントタグを記述します。


<div id="app">
  <slot-test>スロットによる挿入</slot-test>
 <slot-test></slot-test>
</div>

挿入がない場合は、初期値であるデフォルトが表示されます。

slotに初期値がある場合
slotに初期値がある場合

slotに名前をつける

先程まではslotに挿入できる内容は1つでしたが、複数ある場合は表示させる場所を識別するためにslotに名前をつけることができます。


Vue.component('slot-test',{
  template: `<div>
  <h1><slot name="title">タイトル</slot></h1>
  <p><slot name="content">内容</slot></p>
  </div>`,
})

<div id="app">
  <slot-test>
    <div slot="title">vue.jsの基礎</div>
    <p slot="content">vue.jsについて</p>
  </slot-test>
</div>
slotに名前をつける
slotに名前をつける

HTMLのソースを確認すると親側でslotに名前をつけたタグがそのまま表示されることになります。

名前をつけたslotのタグの状態
名前をつけたslotのタグの状態

div, pをtemplateタグに変更すれば、期待通りの表示になります。


<div id="app">
  <slot-test>
    <template slot="title">vue.jsの基礎</template>
    <template slot="content">vue.jsについて</template>
  </slot-test>
</div>
templateタグを使用した結果
templateタグを使用した結果