はじめに
本稿はフロントエンド領域から逃げてきたバックエンドエンジニアが追い詰められて0から勉強した軌跡である。詳細は前回をご参照ください^^
ネコ本とは?
みんな大好きMio様の名著「基礎から学ぶ Vue.js」です。
https://www.amazon.co.jp/dp/B07D9BYHMZ
童貞卒業までの道程
- (済)基本
- (済)データバインディング、条件分岐、繰り返し
- (済)ハンドラ、双方向バインディング
- (済)算出プロパティ
- ウォッチャ、フィルタ
- コンポーネント ←今日はここまで
- VueCLI、単一ファイルコンポーネント
- Vuex
- Vue Router
ウォッチャ
ウォッチャとは、何かしらデータ、ないし算出プロパティの変更を監視、変更をフックに処理を実行するための仕組みを指す。
双方向バインディングしているリアクティブなデータが入力フォームで変更されたりとかですね。
watch
ディレクティブに以下のように記載します。
<html lang="ja">
<head>
</head>
<body>
<div id="app">
topics : <select v-model="current">
<option v-for="topic in topics" v-bind:value="topic.value">
{{topic.name}}
</option>
</select>
<div v-for="item in list">{{item.full_name}}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="main.js"></script>
</body>
</html>
new Vue({
el: "#app",
data: {
list: [],
current: "",
topics: [
{value: 'vue', name: 'Vue.js'},
{value: 'jQuery', name: 'jQuery'}
],
},
watch: {
current: function(val) {
axios.get('https://api.github.com/search/repositories',{
params: {
q: 'topic:'+this.val,
sort: this.sort
}
}).then(function(response) {
this.list = response.data.items
}.bind(this))
}
}
})
上記はプルダウンの値を変更すると、githubのAPIに指定したトピックのリポジトリを取得し、一覧で表示します。
はい。ソートする軸を指定したいですね。
トピックを指定するだけだと、githubはデフォルトで関連度の高い順に表示してくれますが、検索リテラシーが低い人は、関連度というよりもstarやforkが多いものを検索した方が良さそうです。
そのためにはトピックの選択だけでなく、そーとの軸も指定できなければなりません。
更にいうと、複数の入力フォームの変化を監視して処理を実行しなければなりません。
そんな時は、算出プロパティを使って複数の値を監視しましょう。
<!<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
<div id="app">
topics : <select v-model="current">
<option v-for="topic in topics" v-bind:value="topic.value">
{{topic.name}}
</option>
</select>
sort : <select v-model="sort">
<option v-for="sort_item in sort_items" v-bind:value="sort_item.value">
{{sort_item.name}}
</option>
</select>
<div v-for="item in list">{{item.full_name}}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="main.js"></script>
</body>
</html>
new Vue({
el: "#app",
data: {
list: [],
current: "",
sort: "",
topics: [
{value: 'vue', name: 'Vue.js'},
{value: 'jQuery', name: 'jQuery'}
],
sort_items: [
{value: 'stars', name: 'stars'},
{value: 'forks', name: 'forks'},
{value: 'updated', name: 'updated'}
]
},
computed: {
watchTarget: function(){
return [this.current, this.sort]
}
},
watch: {
watchTarget: function(val) {
axios.get('https://api.github.com/search/repositories',{
params: {
q: 'topic:'+this.val,
sort: this.sort
}
}).then(function(response) {
this.list = response.data.items
}.bind(this))
}
}
})
これで、更新日、Star、forkでソートすることができますね。検索リテラシーの低い人も一安心です^^
フィルタ
フィルタは文字列や数字を加工するテキストベースの処理を行う仕組みです。
末尾に「円」をつけたり、数字を桁区切りにしたり、、といった処理をまとめて置くことができます。
<html lang="ja">
<head>
</head>
<body>
<div id="app">
<p>{{price}}</p>
<p>{{price | separator}}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
new Vue({
el: "#app",
data: {
price: 19800
},
filters: {
separator: function(val) {
return val.toLocaleString()
}
}
})
これで、桁区切りをseparator
というフィルタにまとめることができます。
ただ、接頭辞に「¥」とつけられていないですね。場合によっては「$」となる可能性もあるので、separator
のフィルタとは別に独立して定義・利用したいです。
フィルタは引数を渡すことも、連続して繋げることもできます。
<html lang="ja">
<head>
</head>
<body>
<div id="app">
<p>{{price | separator | prefix('ja')}}</p>
<p>{{price | separator | prefix('us')}}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
new Vue({
el: "#app",
data: {
price: 19800
},
filters: {
separator: function(val) {
return val.toLocaleString()
},
prefix: function(val,locale){
switch(locale) {
case 'ja':
return "¥"+val
case 'us':
return '$'+val
default:
return val
}
}
}
})
これで一安心ですね^^
コンポーネント
コンポーネントとは、振る舞いやデータだけでなく、描画するUIやデザインも含めて1まとまりに切り出すことができる仕組みですね。
コンポーネントごとに、UIと振る舞いとデータを独立して書くことができるので、再利用したり、保守時の影響範囲を限定したりと、とても素敵な機能です。やっとフレームワークっぽくなってきましたね^^
コンポーネントは以下のように定義、利用します。
<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
<div id="app">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
Vue.component('my-component', {
template: '<p>Hello World!</p>'
})
new Vue({
el: '#app'
})
Vue.component
でグローバルにコンポーネント「my-component
」を登録することで、<my-component></my-component>
と書くだけでコンポーネントが呼び出されました。
グローバルで定義すると、大規模になってくると影響範囲が怖いので、ローカルに登録したいですね。
そんな時は以下のように記載します。
var myComponent = {
template: '<p>Hello World!!</p>'
}
new Vue({
el: '#app',
components:{
'my-component': myComponent
}
})
また、テンプレートは単一のルート要素が持つように書かなければなりません。
// これはダメで
<p>Hello World!!</p><p>Hello Vue.js!!</p>
// これは良い
<div><p>Hello World!!</p><p>Hello Vue.js!!</p></div>
グローバルで定義した場合、Vue.component
の中のdata
オプションの書き方はオブジェクトを返す関数でなければならない。
Vue.component('my-component', {
template: '<p>{{message}}</p>',
data: function(){
return {
message: 'Hello Vue.js!'
}
}
})
new Vue({
el: '#app'
})
上記のdata
の書き方が今までと少し違いますね。
コンポーネント間の通信(親→子:props down)
コンポーネントの中でコンポーネントを利用する状況が発生しますね。
そんな時、コンポーネント同士は親-子の関係となりますが、親で定義したリアクティブなデータは親コンポーネントのスコープなので、子のコンポーネントでそのまま利用することができません。
困りました。そんな時はprops down
して、リアクティブなデータを子コンポーネントに渡してあげましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
<div id="app">
<parent-component></parent-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
Vue.component('child-component', {
template: '<p>{{val}}</p>',
props: ['val']
})
Vue.component('parent-component', {
template: '<child-component v-bind:val="message"></child-component>',
data: function(){
return {
message: 'Hello Vue.js!'
}
}
})
new Vue({
el: '#app'
})
上記は親コンポーネントparent-component
で定義したリアクティブなデータを子コンポーネントchild-component
のテンプレートで描画していますね。
親はテンプレートの中でv-bind:val=
と、属性を使ってデータを子コンポーネントに渡しています。
子コンポーネントはprops: ['val']
とprops
属性で親から渡されたデータを使える状態にしていますね。
注意点としては、渡されたデータはリアクティブなデータなので、親側の変更を子側に伝えることはそのままできますが、子側でデータを勝手に書き換えてはいけません。
後述する子→親へのデータの受け渡しをするか、算出プロパティを使いましょう。
また、子は親の渡すデータを何も確認せずに受けとって良いのでしょうか?そんな綺麗な親子関係が成り立っていれば世界はもっと幸せです。
子コンポーネントは渡されたデータのチェックを行うことができます。
Vue.component('child-component', {
template: '<p>{{val}}</p>',
props: {
val: {
type: [String,Number],
required: true
}
}
})
上記は、データ型と必須チェックをしています。これで一安心ですね^^
コンポーネント間の通信(子→親:event up)
子で発生したイベントを元に親でアクションを実行して欲しいですよね。
また、親から渡されるデータを直接変更することができないことは前述していますが、親に変更を依頼することはできます。
そんな時は、カスタムイベントと、インスタンスメソッド$emit
を使いましょう。
カスタムイベントはv-on:click
のように、v-on:〇〇
と親側では記述します。簡単ですね。
$emit
は直訳では「発する」「放射する」という意味で、イベントを明示的に発火させるためのメソッドのようです。よくわからないので実際に書いてみましょう。
Vue.component('child-component', {
template: `<li>{{name}} HP. {{hp}}
<button v-on:click="doAttack">攻撃する</button></li>
`,
props: {
id: Number,
name: String,
hp: Number
},
methods: {
doAttack: function(){
this.$emit('attack', this.id)
}
}
})
Vue.component('parent-component', {
template: '<ul><child-component v-for="item in list" v-bind:key="item.id" v-bind="item" v-on:attack="handleAttack"></child-component></ul>',
data: function(){
return {
list: [
{id: 1, name: '田中', hp: 1000},
{id: 2, name: '佐藤', hp: 2800},
{id: 3, name: '山田', hp: 3500}
]
}
},
methods: {
handleAttack: function(id){
var item = this.list.find(function(el){
return el.id === id
})
if (item !== undefined && item.hp > 0) item.hp -= 100
}
}
})
new Vue({
el: '#app'
})
山田に攻撃したくなりますね。
子コンポーネントから親コンポーネントへの受け渡しは、
子コンポーネントで実行されたイベントが実行するmethods
が$emit
メソッドを実行し、$emit
メソッドは親のイベントを発火させ、発火したイベントは親コンポーネントの中で記載されたメソッドを実行し、そのメソッド内で値を変更する。
とった流れです。わかりにくいですね。
ただ、親の中で定義したデータをみだりに子で操作させないためにもイベントを経由するという方法は確かに安心安全な気がします。
終わりに
コンポーネント間のデータ通信は理解するのに予想以上にパワーがかかりました(´ཀ`」 ∠)_