# CH5 コンポーネントで UI 部品を作る

# S23 コンポーネント間の通信 / 親から子

155~160ページ

コンポーネントでプロパティを受け取るためのprops定義
Vue.component('comp-child', {
  // テンプレートで受け取ったvalを使用
  template: '<p>{{ val }}</p>',
  // 受け取る属性名を指定
  props: ['val']
})
プロパティとして文字列を渡す
<comp-child val="これは子A"></comp-child>
<comp-child val="これは子B"></comp-child>
プロパティとしてデータを渡す
<comp-child :val="valueA"></comp-child>
<comp-child :val="valueB"></comp-child>
new Vue({
  el: '#app',
  data: {
    valueA: 'これは子A',
    valueB: 'これは子B'
  }
})
DEMO

これは子A

これは子B

guide-ch5-demo01

※ プロパティでの受け取り方は同じ

# コンポーネントをリストレンダリング

157ページ

# 子コンポーネント

Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}</li>',
  props: ['name', 'hp']
})
<ul>
  <comp-child v-for="item in list"
    v-bind:key="item.id"
    v-bind:name="item.name"
    v-bind:hp="item.hp"></comp-child>
</ul>

# 親コンポーネント

new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'スライム', hp: 100 },
      { id: 2, name: 'ゴブリン', hp: 200 },
      { id: 3, name: 'ドラゴン', hp: 500 }
    ]
  }
})
DEMO
  • スライム HP.100
  • ゴブリン HP.200
  • ドラゴン HP.500
guide-ch5-demo02

# エラーになるパターン

Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}\
  <button v-on:click="doAttack">攻撃する</button></li>',
  props: ['name', 'hp'],
  methods: {
    doAttack: function () {
      // 勝手に攻撃!
      this.hp -= 10 // -> [Vue warn] error!
    }
  }
})

# propsの受け取りデータ型を指定する

159ページ

書籍では表化していませんでしたが、見やすいようにまとめなおし&加筆しています。

# データ型一覧

特定のコンストラクタのインスタンスであるかをチェックできます。

データ型 説明
String 文字列 '1'
Number 数値 1
Boolean 真偽値 true, false
Function 関数 function() {}
Object オブジェクト { name: 'foo' }
Array 配列 [1, 2, 3], [{ id: 1 }, { id: 2 }]
カスタム インスタンス new Cat()
null すべての型 1, '1', [1]
タイプチェックを省略した場合
Vue.component('example', {
  props: ['value'] // どんな型も受け入れる
})
データ型のみチェックする場合
Vue.component('example', {
  props: {
    value: データ型
  }
})
インスタンスのチェック
function Cat(name) {
  this.name = name
}
Vue.component('example', {
  props: {
    value: Cat // 猫データのみ許可!
  }
})
new Vue({
  data: {
    value: new Cat('たま') // valueは猫データ
  }
})
<example v-bind:value="value"></example>

# オプション一覧

オプション データ型 説明
type データ型, 配列 許可するデータ型、配列で複数可能
default データ, 関数 デフォルトの値
required Boolean 必須にする
validator 関数 カスタムバリデータ関数、チェックして真偽値を返す
その他のオプションも使用する場合
Vue.component('example', {
  props: {
    value: {
      type: [String, Number],
      default: 100,
      required: true,
      validator: function (value) {
        return value > 10
      }
    }
  }
})

TIP

想定しない型も受け取ってしまうと、使用するときに毎回最初からチェックする必要があったり、エラーになる場所が増えてしまう。 それなら、想定していないものと分かった時点で、早めにエラーにしてしまう方が対処しやすいね🐾

# S23 コンポーネント間の通信 / 子から親

161~165ページ

# 子のイベントを親にキャッチさせる

161ページ

# 子コンポーネント

子が自分のイベントを起こす
Vue.component('comp-child', {
  template: '<button v-on:click="handleClick">イベント発火</button>',
  methods: {
    // ボタンのクリックイベントのハンドラでchilds-eventを発火する
    handleClick: function () {
      this.$emit('childs-event')
    }
  }
})

# 親コンポーネント

親のテンプレート
<comp-child v-on:childs-event="parentsMethod"></comp-child>
親側で受け取る
new Vue({
  el: '#app',
  methods: {
    // childs-eventが発生した!
    parentsMethod: function () {
      alert('イベントをキャッチ! ')
    }
  }
})
DEMO
guide-ch5-demo03

# 親が持つデータを操作しよう

163ページ

子コンポーネント
Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}\
  <button v-on:click="doAttack">攻撃する</button></li>',
  props: {
    id: Number,
    name: String,
    hp: Number
  },
  methods: {
    // ボタンのクリックイベントのハンドラから$emitでattackを発火する
    doAttack: function () {
      // 引数として自分のIDを渡す
      this.$emit('attack', this.id)
    }
  }
})
親コンポーネント
<ul>
  <comp-child v-for="item in list"
    v-bind:key="item.id"
    v-bind="item"
    v-on:attack="handleAttack"></comp-child>
</ul>
new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'スライム', hp: 100 },
      { id: 2, name: 'ゴブリン', hp: 200 },
      { id: 3, name: 'ドラゴン', hp: 500 }
    ]
  },
  methods: {
    // attackが発生した!
    handleAttack: function (id) {
      // 引数のIDから要素を検索
      var item = this.list.find(function (el) {
        return el.id === id
      })
      // HPが0より多ければ10減らす
      if (item !== undefined && item.hp > 0) item.hp -= 10
    }
  }
})

# S23 コンポーネント間の通信 / 非親子

165~166ページ

var bus = new Vue({
  data: {
    count: 0
  }
})
Vue.component('component-b', {
  template: '<p>bus: {{ bus.count }}</p>',
  computed: {
    // busのデータを算出プロパティに使用
    bus: function () {
      return bus.$data
    }
  },
  created: function () {
    bus.$on('bus-event', function () {
      this.count++
    })
  }
})

# S23 コンポーネント間の通信 / その他

166~168ページ

# 子コンポーネントを参照する$refs

166ページ

親コンポーネント
<comp-child ref="child">
new Vue({
  el: '#app',
  methods: {
    handleClick: function () {
      // 子コンポーネントのイベントを発火
      this.$refs.child.$emit('open')
    }
  }
})
子コンポーネント
Vue.component('comp-child', {
  template: '<div>...</div>',
  created: function () {
    // 自分自身のイベント
    this.$on('open', function () {
      console.log('なにか処理')
    })
  }
})

# S24 スロットを使ったカスタマイズ

169~174ページ

# 名前付きスロット

171ページ

親コンポーネント / スロットコンテンツを定義
<comp-child>
  <header slot="header">
    Hello Vue.js!
  </header>
  Vue.jsはJavaScriptのフレームワークです。
</comp-child>
子コンポーネント / スロットを使用
<section class="comp-child">
  <slot name="header">
    <header>
      デフォルトタイトル
    </header>
  </slot>
  <div class="content">
    <slot>デフォルトコンテンツ</slot>
  </div>
  <slot name="footer">
    <!-- なければ何も表示しない -->
  </slot>
</section>
DEMO
Hello Vue.js!
Vue.jsはJavaScriptのフレームワークです。
guide-ch5-demo06

# S25 コンポーネントの双方向データバインド

175~178ページ

# コンポーネントの v-model

175ページ

v-model のカスタマイズ
Vue.component('my-calendar', {
  model: {
    // 現在の値をvalueではなくcurrentに割り当てる
    prop: 'current',
    // イベントをchangeに割り当てる
    event: 'change'
  },
  // propsでcurrentを受け取る
  props: {
    current: String
  },
  created: function () {
    this.$emit('change', '2018-01-01')
  }
})

# .sync による双方向バインディング

177ページ

親コンポーネント
<my-component v-bind:name.sync="name" v-bind:hp.sync="hp"></my-component>
new Vue({
  el: '#app',
  data: {
    name: 'スライム',
    hp: 100
  }
})
子コンポーネント
Vue.component('my-component', {
  template: '<div class="my-component">\
  <p>名前.{{ name }} HP.{{ hp }}</p>\
  <p>名前 <input v-model="localName"></p>\
  <p>HP <input size="5" v-model.number="localHp"></p>\
  </div>',
  props: {
    name: String,
    hp: Number
  },
  computed: {
    // 算出プロパティのセッター&ゲッターを使ってv-modelを使用
    localName: {
      get: function () {
        return this.name
      },
      set: function (val) {
        this.$emit('update:name', val)
      }
    },
    localHp: {
      get: function () {
        return this.hp
      },
      set: function (val) {
        this.$emit('update:hp', val)
      }
    }
  }
})

# S27 その他の機能やオプション

184~189ページ

# 関数型コンポーネント

184ページ

Vue.component('functional-component', {
  functional: true,
  render: function (createElement, context) {
    return createElement('div', context.props.message)
  },
  props: {
    message: String
  }
})

# 動的コンポーネント

185ページ

子コンポーネント
// コンポーネントA
Vue.component('my-component-a', {
  template: '<div class="my-component-a">component A</div>'
})
// コンポーネントB
Vue.component('my-component-b', {
  template: '<div class="my-component-b">component B</div>'
})
親コンポーネント
<button v-on:click="current^=1">コンポーネントを切り替え</button>
<div v-bind:is="component"></div>
new Vue({
  el: '#app',
  data: {
    // コンポーネントのリスト
    componentTypes: ['my-component-a', 'my-component-b'],
    // 描画するコンポーネントを選択するためのindex
    current: 0
  },
  computed: {
    component: function () {
      // currentと一致するindexのコンポーネントを使用
      return this.componentTypes[this.current]
      // 別に `return current ? 'my-component-b' : 'my-component-a'` とかでもよい
    }
  }
})

# 共通処理を登録するMixin

186ページ

ミックスインを定義
var mixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}
ミックスインを使用
Vue.component('my-component-a', {
  mixins: [mixin], // ミックスインを登録
  template: '<p>MyComponentA</p>'
})
Vue.component('my-component-b', {
  mixins: [mixin], // ミックスインを登録
  template: '<p>MyComponentB</p>'
})

# keep-alive で状態を維持する

188ページ

子コンポーネント×2
// メッセージ一覧用コンポーネント
Vue.component('comp-board', {
  template: '<div>Message Board</div>',
})
// 入力フォーム用コンポーネント
Vue.component('comp-form', {
  template: '<div>Form<textarea v-model="message"></textarea></div>',
  data: function () {
    return {
      message: ''
    }
  }
})
親コンポーネント
<button v-on:click="current='comp-board'">メッセージ一覧</button>
<button v-on:click="current='comp-form'">投稿フォーム</button>
<div v-bind:is="current"></div>
new Vue({
  el: '#app',
  data: {
    current: 'comp-board' // 動的に切り替える
  }
})
keep-alive を使用した場合の親テンプレート
<button v-on:click="current='comp-board'">メッセージ一覧</button>
<button v-on:click="current='comp-form'">投稿フォーム</button>
<keep-alive>
  <div v-bind:is="current"></div>
</keep-alive>