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({
  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データ, 関数デフォルトの値
requiredBoolean必須にする
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>