JavaScript
Node.js
vue.js
pug

初心者がVue.jsの公式ガイドを勉強するメモ コンポーネント編 第一部

コンポーネント編 第一部

お題はこちらです。

https://jp.vuejs.org/v2/guide/components.html

  • ブラウザー結果は、掲載しませんので、実際に確認してみてください。
  • この章は、ボリュームがあります。
  • コンポーネントは、Vue.jsの強力な機能だと思いますが、初心者は、まず使わない機能だとも思います。 初心者編として少し矛盾する気もしますが、初心者でも色々な初心者がいると思いますので、必要と思っている初心者向けに書きたいと思います。 少しづつでも機能を使うことが、次のステップに上がるのに必要と感じています。
  • 主に、slot関係以下が、第二部になります。

グローバル登録

  • App.vue
<template lang="pug">
  #app
    my-component

</template>
<script>

import Vue from 'vue'

const MyComponent = {
  template: `<div> Custom component</div>`
}
Vue.component( MyComponent )
// Vue.component('my-component', MyComponent)

export default {
  name: 'app',
  data () {
    return {
      checked: '',
      checkedNames: [],
    }
  },
  mounted () {
  },
  components: {
    'my-component': MyComponent
  }
}

</script>

ローカル登録

  • App.vue
<template lang="pug">
  #app
    my-component

</template>

<script>

var Child = {
  template: '<div> Local Custom Component </div>'
}

export default {
  name: 'app',
  data () {
    return {
    }
  },
  components: {
    'my-component': Child
  }
}

</script>

DOM テンプレート解析の注意事項

  • index.htmlのみの環境で影響を受けます。 文字列テンプレートでは無いです。

  • node環境は文字列テンプレートです。 こちらを使いましょう。

<template lang="pug">
  #app
    local-component
    local-component
    local-component

    div -------------------------------

    global-component
    global-component
    global-component

</template>

<script>

import Vue from 'vue'

const GlobalComponent = {
  template: `<button @click="counter += 1">{{ counter }}</button>`,
  data: function () {
    return data
  }
}
Vue.component( 'global-component', GlobalComponent )

var data = { counter: 0 }
var LocalComponent = {
  template: '<button @click="counter += 1">{{ counter }}</button>',
  data: function () {
    return data
  }
 }

export default {
  name: 'app',
  data () {
    return {
    }
  },
  components: {
    'localComponent': LocalComponent
  }
}

</script>

  • カウンターは、参照値で同一の値を参照していることを確認.

  • App.vue

<template lang="pug">
  #app
    local-component
    local-component
    local-component

</template>

<script>
var Child = {
  template: '<button @click="counter += 1">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>
  • カウンターは個別になるのを確認

プロパティ

プロパティによるデータの伝達

  • App.vue
<template lang="pug">
  #app
    globalChild(message="hello")
    div --------------------------
    localComponent(message="localHello")

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['message'],
  template: '<span>{{ message }}</span>'
})

var Child = {
  template: '<span>{{ message }}</span>',
  props: ['message'],
  data () {
    return {
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>
  • 慣れるまでは、両方のコンポーネント記述します。
  • 面倒になったら、片方にします。(笑)

  • コンポーネント内と外とのデータの受け渡しが厳密なことに、注目してください。
  • コンポーネントを、App.vueに書いていますが、別ファイルにしたほうがより、汎用的です。
  • コンポーネントは、本来たくさんの場所から呼び出される、雛形ファイルなので、データの受け渡しが厳密だと解釈するとイメージし易いかと思います。

キャメルケース vs ケバブケース

  • App.vue
<template lang="pug">
  #app
    globalChild(my-message="hello")
    globalChild(myMessage="hello")
    div --------------------------
    localComponent(my-message="localHello")
    localComponent(myMessage="localHello")

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['myMessage'],
  template: '<span> {{ myMessage }}</span>'
})

var Child = {
  template: '<span> {{ myMessage }}</span>',
  props: ['myMessage'],
  data () {
    return {
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>
  • node環境で記載する場合、どちらで書いても構いません。
  • 独自タグもnode環境であれば、どちらでもかまいません。例はcamel caseしかありませんが。。。

動的なプロパティ

  • App.vue
<template lang="pug">
  #app
    input(v-model="text")
    global-child(:myMessage="text")
    localComponent(:myMessage="text")

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['myMessage'],
  template: '<span> {{ myMessage }}</span>'
})

var Child = {
  template: '<span> {{ myMessage }}</span>',
  props: ['myMessage'],
  data () {
    return {
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
      text: 'sample'
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>
  • 省略記号を使っていますが、慣れるまではv-bind:myMessageとしたほうがいいかもしれません。
  • 最初は、変数(key)なのか文字列なのか判りにくいです。
  • 親要素のdataをpropsを使って、引き渡しています。 バインド先は、各コンポーネントです。

v-bind:プロパティ名 の代わりに v-bind

  • App.vue
<template lang="pug">
  #app
    globalChild(:todo="todoObj")
    div ----------------------------
    localComponent(
      :text="todoObj.text"
      :isComplete="todoObj.isComplete"
    )

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['todo'],
  template: '<span> {{ todo.text }} : {{ todo.isComplete}}</span>'
})

var Child = {
  template: '<span> {{ text }} : {{ isComplete }}</span>',
  props: ['text', 'isComplete'],
  data () {
    return {
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
      todoObj: {
        text: 'Learn Vue',
        isComplete: false
      }
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>

  • 引数なし、動きません! だれか教えて下さい. Question 02
  • 一応、todoを引数にすれば、todoObjの塊として渡せてますが、引数ついてるし、違うということですよね。

  • App.vue

<template lang="pug">
  #app
    globalChild(v-bind="todoObj")
    div ----------------------------
    localComponent(
      :text="todoObj.text"
      :isComplete="todoObj.isComplete"
    )

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['text', 'isComplete'],
  template: '<span> {{ text }} : {{ isComplete}}</span>'
})

var Child = {
  template: '<span> {{ text }} : {{ isComplete }}</span>',
  props: ['text', 'isComplete'],
  data () {
    return {
    }
  }
 }
export default {
  name: 'app',
  data () {
    return {
      todoObj: {
        text: 'Learn Vue',
        isComplete: false
      }
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>
  • 自己解決しました。 (分割代入と同じ理屈です)
  • 親要素のdata propertyを塊で取得して、必要なプロパティのみ使う感じです。
  • スッキリ書けるので、いいですね。
  • 上の書き方も有りかと思うので、そのまま追記します。

リテラル vs 動的

  • App.vue
<template lang="pug">
  #app
    globalChild(:some-prop="1")
    globalChild(some-prop="1")

    div --------------------------

    localComponent(:someProp="2")
    localComponent(someProp="2")

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['someProp'],
  template: '<span>Number? {{ someProp + 1 }} </span>'
})

var Child = {
  template: '<span>Number? {{ someProp + 1 }}</span>',
  props: ['someProp'],
  data () {
    return {
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>
  • v-bind付きは、変数と思っていましたが、数字を渡すと数字として引き渡されんですね。
  • 仕組みはJSの式なんですね。
  • これは、やってしまいそうな間違いです。

単方向データフロー

  • App.vue
<template lang="pug">
  #app
    | ParentData: {{ parentData }}
    globalChild(v-bind="parentData")

    div --------------------------

    localComponent(:size=" parentData.size ")

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['parentNumber'],
  template: `
    <div>
      <div>Case_1 - ParentNumber: {{ parentNumber }}</div>
      <div>Case_1 - Counter: {{ counter }}</div>
      <button @click="parentNumber = 2">direct write</button>
    </div>
  `,
  data () {
    return {
      counter: this.parentNumber,
    }
  },
  computed: {
  }
})

var Child = {
  template: `
    <div>
      <div>Case_2: {{ size }}{{ size }}</div>
      <div>Case_2: {{ normalizeSize }}{{ normalizeSize }}</div>
      <button @click="size = 'BBB'">direct write</button>
    </div>
  `,
  props: ['size'],
  data () {
    return {
    }
  },
  computed: {
    normalizeSize () {
      return this.size.trim().toLowerCase()
    }
  }
 }

export default {
  name: 'app',
  data () {
    return {
      parentData: {
        parentNumber: 1,
        size: 'AAA'
      }
    }
  },
  components: {
    'localComponent': Child
  }
}

</script>

  • Case_1 は、親から貰った、値をコンポーネントの初期値にする場合
  • Case_2 は、親から貰った、値を加工して使う場合です。
  • 注意書きは、親から貰った値が配列、オブジェクトであった場合、そのデータを書き換えると親データが書き換えられるので、一旦置き換えて使ってくださいとのことです。
  • もちろん、表示データとしてなら、そのまま使えばいいです。
  • 試しに書き換えようとすると、コンソールにエラーがでますね。
    • コンポーネント側は書き換わりますが、親側には影響無かったです。
    • いずれにしても、propsの値をコンポーネント側で書き換えるのはNGということです。

プロパティ検証

  • 受け取るプロパティを制限できる見たいです。
  • 初心者は、まず数字と文字列の制限から始めるのもいいですね。

プロパティではない属性

  • 飛ばします

カスタムイベント

カスタムイベントとの v-on の使用

  • App.vue
<template lang="pug">
  #app
    p {{ total }}
    button-counter(@increment="incrementTotal")
    button-counter(@increment="incrementTotal")

</template>

<script>
import Vue from 'vue'

Vue.component('button-counter', {
  template: `<button @click="incrementCounter"> {{ counter }} </button>
  `,
  data () {
    return {
      counter: 0,
    }
  },
  methods : {
    incrementCounter () {
      this.counter += 1
      this.$emit('increment')
    }
  }
})

export default {
  name: 'app',
  data () {
    return {
      total: 0
    }
  },
  methods: {
    incrementTotal () {
      this.total += 1
    }
  },
  components: {
  }
}

</script>
  • 基本動作ですが、慣れていないと少し複雑な感じをうけますね。
  • 親と子コンポーネントは完全に別処理であることを確認。
  • @increment="incrementTotal でemitが実行されたタイミングで親側関数が呼ばれます。

ペイロードのデータ利用

  • コンポーネントのデータを親のdataに追加
<template lang="pug">
  #app
    p(v-for="msg in messages") {{ msg }}
    button-message(@message="handleMessage")

</template>

<script>
import Vue from 'vue'

Vue.component('button-message', {
  template: `<div>
    <input type="text" v-model="message"/>
    <button @click="handleMessage"> Send </button>
  </div>`,
  data () {
    return {
      message: 'component Message'
    }
  },
  methods : {
    handleMessage () {
      this.$emit('message', { message: this.message })
    }
  }
})

export default {
  name: 'app',
  data: () => ({
    messages: ['parent 1', 'parent 2']
    }),
  methods: {
    handleMessage (payload) {
      this.messages.push(payload.message)
    }
  },
  components: {
  }
}

</script>
  • オブジェクトでデータを渡しています。

ネイティブイベントとコンポーネントのバインディング

  • コンポーネントでは、v-onは、子要素からのemitの監視に使いますが、例の場合、親要素のクリックイベントとして使いたい場合は、native修飾子を使うと、親側のイベントとして動作します。

.sync 修飾子

  • App.vue
<template lang="pug">
  #app
    div {{ bar }}
    //- globalChild(:foo="bar" @update:foo="val => bar = val")
    globalChild(:foo.sync="bar")

</template>

<script>
import Vue from 'vue'

Vue.component('globalChild', {
  props: ['foo'],
  template: `
    <div>
      <button @click="aaa">click</button>
      <input type="text" v-model="text" />
    </div>
  `,
  data () {
    return {
      text: ''
    }
  },
  methods: {
    aaa () {
      this.$emit('update:foo', this.text)
    }
  }
})

export default {
  name: 'app',
  data () {
    return {
      bar: 'ParentValue'
    }
  },
  components: {
  }
}

</script>


  • v-on は使っていません, emitが実行され、値がupdateしています。
  • コンポーネント側のdata プロパティを親のdataプロパティとをクリックイベントで同期させています。
  • コメントで消しているテンプレートでも、同じ動作です。
  • 親側の処理が省略出来る感じですね。

カスタムイベントを使用したフォーム入力コンポーネント

  • App.vue
<template lang="pug">
  #app
    div {{ something }}
    input(v-model="something")

    div {{ something2 }}
    input(
      v-bind:value="something2"
      v-on:input="something2 = $event.target.value"
    )

</template>

<script>

export default {
  name: 'app',
  data () {
    return {
      something: 'v-model',
      something2: 'v-bind v-on',
    }
  },
  components: {
  }
}

</script>

  • まずは、コンポーネントを使っていない例です。
  • 結果は、同じです。

component

  • App.vue
<template lang="pug">
  #app
    | ParentValue : {{ parent }}
    div
      custom-input(:input.sync="parent")

</template>

<script>

import Vue from 'vue'

Vue.component('custom-input', {
  template: `
    <span>
      <input
        :value="value"
        @input="updateValue($event.target.value)"
      />
    </span>
  `,
  props: ['value'],
  methods: {
    updateValue (value) {
      this.$emit('update:input', value)
    }
  }
  })

export default {
  name: 'app',
  data () {
    return {
      parent: 'Parent'
    }
  },
  components: {
  }
}
</script>
  • .sync修飾子は、このあたりが楽できますね。

exapmple

  • App.vue
<template lang="pug">
  #app
    div {{ price }}
    currency-input(v-model="price")

</template>

<script>

import Vue from 'vue'

Vue.component('currency-input', {
  template: `
    <span>
      <input
        ref="input"
        v-bind:value="value"
        v-on:input="updateValue($event.target.value)"
      />
    </span>
  `,
  props: ['value'],
  methods: {
    updateValue (value) {
      let formattedValue = value
      .trim()
      .slice(
        0,
        value.indexOf('.') === -1
          ? value.length
          : value.indexOf('.') + 3
        )
        if (formattedValue !== value) {
          this.$refs.input.value = formattedValue
        }
        this.$emit('input', Number(formattedValue))
    }
  }
  })

export default {
  name: 'app',
  data () {
    return {
      price: ''
    }
  },
  components: {
  }
}
</script>
  • v-model を分解した、v-bindとv-on と見比べるとなんとなく、連携が見えて来る感じがします。
  • value -> props, emit -> input で受け渡しですね。
  • ボリュームありますので、第一部ということで、今回はここまでにします。
  • 主にslot関係が残っていますね.