今回は、Vue.js の重要な概念の一つであるコンポーネントについて紹介します。
Vue.js の書き方を最初から知りたい方はこちら(^^)
参考書

コンポーネントとは
アプリケーションのUIは、複数の部品の組み合わせでできています。
ECサイトを例にすると、「検索フォーム」「商品一覧」「ページ送り」などの部品から成り立っていますね。これらの部品をコンポーネントと呼びます。
これらを全てHTMLに直接記述するよりも、コンポーネントごとに管理することで再利用性や保守性を高めることができますね。
以下にその方法を紹介していきます。
コンポーネントの定義方法
Vue.component() を使うことで、グローバルスコープにコンポーネントを登録できます。よって、どこからでもコンポーネントを参照できるようになります。
そして、このコンポーネントを使う場所に < コンポーネントの名前 ></ コンポーネントの名前 > というカスタムタグを記述します。
では、’ Hello ‘ と描画するだけの簡単なサンプルを見てみましょう。
<div id="app">
<sample></sample>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="sample.js"></script>
<script src="main.js"></script>
// main.js
var app = new Vue({
el: '#app'
});
// sample.js
Vue.component('sample', {
template: '<p>Hello</p>'
});
これで、ブラウザには ‘ Hello ‘ と表示されます。
このように、< コンポーネントの名前 ></ コンポーネントの名前 >と記述した場所に、そのコンポーネントの template オプションの内容が描画されます。
Vue.component() の構造
Vue.component() の第二引数はオブジェクトになっており、new Vue() と同様に data や methods などのオプションを指定できます。
ただ、Vue.component() の data オプションは、{ プロパティ名:値 } を返す関数としなければいけません。
また、template オプションに記述するテンプレートは、必ず全体を単一のタグで囲む必要があります。
以下は、誤った記述です。
// sample.js
//誤った記述
Vue.component('sample', {
template: '<span>{{name}}</span>:<span>{{price}}</span>',
data: {
name: '五円チョコ',
price: '5円'
}
});
正しい書き方はこちら
// sample.js
//正しい記述
Vue.component('sample', {
template: '<p><span>{{name}}</span>:<span>{{price}}</span></p>',
data: function() {
return {
name: '五円チョコ',
price: '5円'
}
}
});
二つの <span> タグを囲む <p> タグを追加し(なんでもよい)、data オプションにはオブジェクトを返す関数を記述することで正しい書き方になりました。
また、テンプレート内で改行するには、全体をバッククォート「`」で囲むと可能になります。
// sample.js
Vue.component('sample', {
template: `<p>
<span>{{name}}</span>:<span>{{price}}</span>
</p>`,
data: function() {
return {
name: '五円チョコ',
price: '5円'
}
}
});
ローカルスコープへの登録
親コンポーネントの components オプションに子コンポーネントを定義することでローカルスコープとなり、その親コンポーネント以外からは参照できないようにすることができます。
<div id="app">
<sample></sample>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app',
components: {
'sample' : sampleChild
}
});
// sample.js(子コンポーネント)
var sampleChild = {
template: '<p>{{message}}</p>',
data: function() {
return {
message: 'Hello'
}
}
};
子コンポーネントをオブジェクト表記で記述し、それを変数に入れます。(この場合は ‘ sampleChild ‘)
親コンポーネントでは、components オプションのプロパティ名に、テンプレートを入れるカスタムタグ名を記述し(この場合は ‘ sample ‘)、プロパティ値に子コンポーネントの変数名(この場合は ‘ sampleChild ‘)を記述することで紐づけることができます。
親コンポーネント以外からは参照できないことを確認しましょう。
<div id="app">
<sample></sample>
</div>
<div id="app2">
<sample></sample>
</div>
検証ツールで見てみると、
親コンポーネントである id=”app” の <div> タグ内がテンプレートに置き換わっているのに対し、
id=”app2″ の <div> タグ内は置き換わっていないことが確認できました。

データの受け渡し
親→子
親コンポーネントから子コンポーネントにデータを渡す場合、子コンポーネントの props オプションにそのデータのプロパティ名を指定します。
そして、親のテンプレートでカスタムタグに属性を追加します。
サンプルを見てみましょう。
<div id="app">
<sample name="五円チョコ" price="5"></sample>
<sample name="うまい棒" price="10"></sample>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app'
});
// sample.js(子コンポーネント)
Vue.component('sample', {
template: `
<p>
<span>{{name}}</span>:<span>{{price}}円</span>
</p>`,
props: ['name', 'price']
});
下のようなイメージでデータを渡しています。

ブラウザの表示結果↓

親のデータを子にバインドする
親コンポーネントにデータを持たせ、カスタムタグに v-bind でデータをバインドします。
<div id="app">
<sample v-bind:name="name" v-bind:price="price"></sample>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app',
data: {
name: '五円チョコ',
price: 5
}
});
// sample.js(子コンポーネント)
Vue.component('sample', {
template: `
<p>
<span>{{name}}</span>:<span>{{price}}円</span>
</p>`,
props: ['name', 'price']
});
少し複雑ですが、下のイメージでデータを渡しています。

ブラウザの表示結果↓

子→親
子から親へは属性を介してデータを渡すことはできないため、まず親は子からデータを受け取るためのイベントハンドラを用意し、子はしかるべきタイミングで親のイベントハンドラを呼び出します。
その際に、データをイベントハンドラの引数として渡します。
サンプルでは、子コンポーネントのボタンをクリックしたときに親コンポーネントのメソッドを呼び出します。
<div id="app">
<sample v-on:click-child="powerUp" v-bind:power="power"></sample>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app',
data: {
power: 18000
},
methods: {
powerUp: function() {
this.power += 1000;
}
}
});
// sample.js(子コンポーネント)
Vue.component('sample', {
template: `
<div>
<button v-on:click="clickHandler">上昇</button> 戦闘力:{{power}}
</div>`,
props: ['power'],
methods: {
clickHandler: function() {
this.$emit('click-child');
}
}
});
流れとしては以下のようになります。
①ボタンをクリック
②子コンポーネントが”clickHandler”を実行
③子コンポーネントに ‘click-child’ イベントを発生
④親のテンプレートが ‘click-child’ イベントを検知し “powerUp”を実行
※’click-child’はカスタムイベントなので自由に名前を付けてよいです。

では次に、データを引数として子から親に渡すサンプルを見てみます。
今回は、戦闘力が22000を超えると「戦闘力:error スカウターが壊れました」と表示されるようにします。
先ほどはパワーアップのメソッドを親コンポーネントに書いていましたが、保守性を考慮すると子に記述したほうがコンポーネントの独立性が高まります。
具体的な数値の変更や条件分岐などは子コンポーネントに処理させ、その結果のデータを親に渡すという書き方がよいでしょう。
<div id="app">
<sample v-on:click-child="powerUp" v-bind:power="power"></sample>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app',
data: {
power: 18000
},
methods: {
powerUp: function(increasedPower) {
this.power = increasedPower;
}
}
});
// sample.js(子コンポーネント)
Vue.component('sample', {
template: `
<div>
<button v-on:click="clickHandler">上昇</button> 戦闘力:{{power}}
</div>`,
props: ['power'],
methods: {
clickHandler: function() {
var increasedPower;
if (this.power < 22000) {
increasedPower = this.power + 1000;
}else if (this.power == 22000) {
increasedPower = 'error スカウターが壊れました';
}else {
//error と表示された後にボタンが押されたときは何もしない
return;
}
this.$emit('click-child', increasedPower);
}
}
});
“clickHandler” の中に increasedPower という変数を用意しました。
power プロパティ値が22000を超えるまでは、power プロパティ値に1000足したものをこの変数に入れます。
そしてこの変数が、 $emit() メソッドの第2引数として親コンポーネントの powerUp メソッドに渡され処理されるという流れです。
ブラウザの表示↓


コンポーネントを繰り返し描画する
親が持つ配列データを、v-for を使うことで子に渡すことができます。
<div id="app">
<ul>
<sample v-for="student in students"
v-bind="student"
v-bind:key="student.number">
</sample>
</ul>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app',
data: {
students: [
{number: '1', name: '阿部', score: 95},
{number: '2', name: '安藤', score: 38},
{number: '3', name: '石川', score: 60},
{number: '4', name: '井上', score: 25},
{number: '5', name: '江崎', score: 76}
]
}
});
// sample.js(子コンポーネント)
Vue.component('sample', {
template: `<li>{{number}} {{name}} {{score}}点</li>`,
props: ['number', 'name', 'score']
});
props: [‘number’, ‘name’, ‘score’] に違和感を抱いたかもしれませんが、
親のテンプレートでオブジェクトを v-bind すると、オブジェクトが持つプロパティを一括でバインドできます。
つまり、
v-bind=”student” は
v-bind:number=”student.number” v-bind:name=”student.name” v-bind:score=”student.score”
と同じ意味になるわけですね。
なので props で参照することができています。
ブラウザの表示結果↓

コンポーネントをHTMLタグとしてブラウザに認識させる
まずは、サンプルで問題のある書き方を見てもらいます。
<div id="app">
<table>
<tbody>
<sample v-for="student in students"
v-bind="student"
v-bind:key="student.number">
</sample>
</tbody>
</table>
</div>
// main.js(親コンポーネント)
var app = new Vue({
el: '#app',
data: {
students: [
{number: '1', name: '阿部', score: 95},
{number: '2', name: '安藤', score: 38},
{number: '3', name: '石川', score: 60},
{number: '4', name: '井上', score: 25},
{number: '5', name: '江崎', score: 76}
]
}
});
// sample.js(子コンポーネント)
Vue.component('sample', {
template: `<tr><td>{{number}}</td><td>{{name}}</td><td>{{score}}点</td></tr>`,
props: ['number', 'name', 'score']
});
一見何も問題なさそうに見えますが、検証ツールでDOMを見てみると、

<tr> タグがきちんと <table> タグに囲まれていないことがわかります。
なぜこのような不具合が起きるかというと…
全てのHTML要素は、親や子にできる要素が決まっています。
例)
<tr> の子要素にできるのは <th> または <td>
<select> の子要素にできるのは <option> または <optgroup>
Vue が <sample> カスタムタグをテンプレートに変換する前に、ブラウザが <sample> タグとして認識してしまうからなんですね。

<tbody> の子要素に <sample> タグは置けないんだよな
(なんやこのタグ……ワシ知らんぞ…)
この問題を回避するには、親のテンプレートに <tr> と書くしかありません。
ブラウザが <tr> タグと認識した後、Vue にはコンポーネントであることを伝えるために
is 属性を使います。
<div id="app">
<table>
<tbody>
<tr is="sample" v-for="student in students"
v-bind="student"
v-bind:key="student.number">
</tr>
</tbody>
</table>
</div>
これで、ブラウザには <tr> タグとして、Vue にはコンポーネントとして認識させることができました。

今回は以上になります。
ご覧いただきありがとうございました(^^)
続きはこちら↓
参考書

コメント