Djangoroidの奮闘記

python,django,angularJS1~三十路過ぎたプログラマーの奮闘記

Vue.js 入門 vol.6 コンポーネント 前半

参考サイト

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

コンポーネントの使用

  • 登録
new Vue({
  el: '#some-element',
})

Vue.component('my-component',{
  //オプション
})
  • 一度登録すると、カスタム要素<my-component>を使えるようになる。親のinstanceのtemplate内で使えるようになる。
<div id="example">
  <my-component></my-component>
</div>
// 登録する
Vue.component('my-component',{
  template: '<div>A custom component!</div>'
})

// rootインスタンスを作成する

new Vue({
  el: '#example'
})
<!-- 上記の結果描画されるhtmlは以下の通り -->
<div id="example">
  <div>A custom component!</div>
</div>

ローカル登録

var Child = {
  template: '<div>A custom component!</div>'
}

new Vue({
  ...
  components: {
    'my-component': Child
  }
})
  • 今のところ、グローバル登録との違いがわからない。。。

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

  • 親・子 elementの関係で、うまく custom elementが読み込まれない時がある。例えば、table - trの関係とか。その場合は、isを使う
<!-- これだとうまく読み込まれない -->
<table>
  <my-row>...</my-row>
</table>

<!-- こっちが推奨される -->
<table>
  <tr is="my-row"></tr>
</table>

data は関数でなければならない

  • Vueコンストラクタ(new Vue({}))に渡すことのできるほとんどのオプションは、コンポーネントの中で利用できる。ただし、dataだけは関数でなければならない。
<div id="example-2">
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
</div>
var data = { counter: 0 }
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // data は技術的には関数なので、Vue は警告を出しません。
  // しかし、各コンポーネントのインスタンスは
  // 同じオブジェクトの参照を返します。
  data: function () {
    return {
    counter: 0
    }
  }
})
new Vue({
  el: '#example-2'
})

コンポーネントの構成

  • props down, events upがキーワード!親は、 プロパティを経由して、データを子に伝え、子はイベントを経由して、親にメッセージを送ります。

プロパティ

  • プロパティによるデータの伝達: propsオプションを利用して、伝達のための変数を明示的に宣言しておく!
Vue.component('child',{
  props:['message'], // この場合は、messageという変数を通じて、親からデータを受け取る。
  template: '<span>{{ message }}</span>'
})
  • 上記のように設定しておくと、以下のように、親のcomponent内で、messageという変数を通じて、valueを受け取ることができる。
<child message="hello!"></child>

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

  • htmlの属性は、大文字と小文字を区別しない。そのため、キャメルケース(大文字含む文字列)のプロパティは、ケバブケース (kebab-case: ハイフンで句切られた) に変更する必要があり。
Vue.component('child', {
  // JavaScript ではキャメルケース
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- HTML ではケバブケース に変換する必要あり-->
<child my-message="hello!"></child>

動的なプロパティ

<div>
  <!--  v-modelで、親componentのparentMsgに、inputのvalueを渡す-->
  <input v-model="parentMsg">
  <br>
  <!-- parentMsgのvalueをv-bindを通じて、親componentの、my-message(myMessage) propsに渡す -->
  <!--  myMessage(my-message)を通じて 子componentにparentMsgのvalueが渡される-->
  <child v-bind:my-message="parentMsg"></child>
</div>

リテラル vs 動的

  • 文字列と、数字を間違えないように注意。実際に JavaScript の数を渡したい場合は、その値が JavaScript の式として評価されるよう、v-bind を使う必要があります
<!-- これは純粋な文字列"1"を渡します -->
<comp some-prop="1"></comp>

<!-- これは実際の数を渡します -->
<comp v-bind:some-prop="1"></comp>

単方向データフロー

  • すべてのプロパティは、親->子のみ。逆は無し!

  • ただし、プロパティは初期値を渡すためにのみ使われ、子コンポーネントは単にその値をローカルデータプロパティとして使用したい場合と、プロパティは変換が必要な生の値として渡されます。の場合は、プロパティを変更したくなる。(よくわからん)

// プロパティの初期値をその初期値とするようなローカルデータプロパティを定義します。
props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}
// プロパティの値から計算される算出プロパティ (computed property) を定義します。

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

プロパティ検証

  • 以下のようにvalidationできる。
Vue.component('example', {
  props: {
    // 基本な型チェック (`null` はどんな型でも受け付ける)
    propA: Number,
    // 複数の受け入れ可能な型
    propB: [String, Number],
    // 必須な文字列
    propC: {
      type: String,
      required: true
    },
    // デフォルト値
    propD: {
      type: Number,
      default: 100
    },
    // オブジェクトと配列のデフォルトはファクトリ関数から返すようにしています
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // カスタムバリデータ関数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})
  • typeの種類は以下の通り。
String
Number
Boolean
Function
Object
Array
Symbol
  • type はカスタムコンストラクタ関数とすることもでき、アサーションは instanceof チェックで作成できるでしょう。らしいが、意味はよくわかってない。たぶん、コンストラクタ関数の型(?)と一致しているかどうかをチェックできるということだろう。

カスタムイベント

  • カスタムイベントとの v-on の使用: イベントが起こったときに、子→親に通信する方法。以下の3つを利用できる。
    • $on(eventName)を使用してイベントを購読します。
    • $emit(eventName)を使用して自身にイベントをトリガーします。
    • コンポーネントは、子コンポーネントが使われているテンプレート内で直接 v-on を使用することで、子コンポーネントからのイベントを購読することができ
<div id="counter-event-example">
  <p>{{ total }}</p>
  <!-- incrementが実行されたときに、incrementTotalを実行する -->
  <!-- いつincrementが実行されるかというと、それは、button-counter component内にある -->
  <!-- v-on:click="increment"なので、buttonがclickされたときに、incrementが起動する -->
  <!-- incrementが起動すると、$emit('increment')により、親方向に、incrementのevent通知が伝えられる。 -->
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment') //$emit('')で、自分を含む親方向へのイベント通知
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})
  • 上記のコードでは、子->親へイベント通知をしているだけで、データの受け渡しなどはされていない。

  • ネイティブイベントとコンポーネントバインディング: コンポーネントの root 要素でのネイティブイベントを購読したいときがあるかもしれません。このような場合、v-on に .native 修飾子を使用することができます。…よくわからん。とにかく例は以下の通りらしい。

<my-component v-on:click.native="doTheThing"></my-component>

.sync 修飾子

  • .syncは双方向データバインディングができるんやな。結構中核になりそうな機能の1つやな。
<comp :foo.sync="bar"></comp>
<!-- ↑は、下のコードに展開される -->
<comp :foo='bar' @update:foo="val => bar = val"></comp>
<!-- 省略記法をなくすと以下の通り -->
<comp v-bind:foo='bar' v-on:update:foo="val => bar = val"></comp>

<!-- v-onで、まずupdateを検知する -->
<!-- それをきっかけに、fooのvalを、barに代入する -->
<!-- v-bindで、barのvalueを、親componentのfooに代入する -->
  • お〜〜〜、これはめっちゃ便利そうだし、使い所多そうだな。

  • 上記のコードで、子コンポーネントが、fooの値を更新するために、プロパティの変更の代わりに、明示的にイベントを発信する必要がある。

this.$emit('update:foo', newValue)
// fooがupdateされました!という情報を親コンポーネントに伝える!

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

  • カスタムイベントは、v-modelとともに、動くカスタムフォーム入力を作成されるためにも利用される。
<input v-model="something">

<!-- 上記のコードは以下のように展開されている -->
<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">
<!-- inputがされたときに、something に、envetのvalueが代入される -->
<!--  somethingが、valueに代入される -->
<custom-input
  :value="something"
  @input="value => { something = value }"
>
</custom-input>
  • つまり、v-modelと一緒に、componentを動かすには以下が必要。

    • value プロパティを受け入れる
    • 新しい値とともに、inputイベントを発信する
  • 通過入力を例は以下の通り。

<currency-input v-model="price"></currency-input>
Vue.component('currency-input',{
  template: `
    <span>
      $
      <input
        ref="input"
        v-bind:value="value"
        v-on:input="updateValue($event.target.value)">
    </span>
  `,
  props: ['value'],
  methods: {
    // 値を直接的に更新する代わりに、このメソッドを使用して
    // inputの値の整形と値に対する制約が行われr。
    updateValue: function(value){
      var formattedValue = value
        //両端のスペースを削除
        .trim()
        // 小数点2桁以下まで短縮
        .slice(
          0,
          value.indexOf('.') === -1
          ? value.length
          : value.indexOf('.') + 3
        )
        // 値がすでに正規化されていないならば
        // 手動で適合するように上書き
        if (formattedValue !== value ){
          this.$refs.input.value = formattedValue
        }
        // input イベントを通して数値を発行する
        this.$emit('input', Number(formattedValue))
    }
  }
  })

コンポーネントの v-model のカスタマイズ

  • v-modelをカスタマイズする。これはdjangoのget_contextなどを上書きするのに似てるな。

  • デフォルトだと、v-modelは、valueをプロパティとして、inputをイベントとして利用する。ただ、チェックボックスラジオボタンなどの入力タイプを利用する場合は、valueプロパティをその他の目的で利用することができる。

Vue.component('my-checkbox',{
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    // これによって、valueプロパティを別の目的で利用することを許可する
    value: String
  }
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

<!-- 上記は以下と同じ -->
<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

親子間以外の通信

スロットによるコンテンツ配信

  • 複数のコンポーネントを使用するときの注意事項。例えば以下のような例。appは独自のtemplateをもっており、app-headerなどもそれぞれ独自のtemplateをもっている。そのため、appがどのコンテンツにどのように作用するかがわかりづらい構造になっている。
<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>
  • コンポーネントのコンテンツと、コンポーネント自身のテンプレートをうまく織り交ぜる方法が必要。これは、コンテンツ配信と呼ばれるプロセスらしい。

  • Vue.jsでは、要素を利用して、配信APIを実装するらしい。

コンパイルスコープ

<child-component>
  {{ message }}
</child-component>
  • 親テンプレート内の全てのものは親のスコープでコンパイルされ、子テンプレート内の全てものは子のスコープでコンパイルされる が鉄則。
<!--someChildProperty が子コンポーネントのプロパティとすると動作しません -->
<!-- なぜなら, このコンテンツは、親コンポーネントに束縛されているから -->
<child-component v-show="someChildProperty"></child-component>
  • 上記の例をchild-componentで機能させたい場合は、以下のように、子component内のtemplateに記載する。
Vue.componet('child-component',{
  template: `<div v-show="someChildProperty>Chile</div>"`,
  data: function(){
    return {
      someChildProperty:true
    }
  }
})
  • 同様に、配信コンテンツは、親スコープでコンパイルされる。配信コンテンツというのは、レンダリングされた後の、templateのことかな。

単一スロット

  • my-component というcomponetのtemplate
<!-- my-component というcomponetのtemplate -->
<div>
  <h2>I'm the child title</h2>
  <slot>
    This will only be displayed if there is no content
    to be distributed.
  </slot>
</div>
<div>
  <h1>I'm the parent title</h1>
  <my-component>
    <p>This is some original content</p>
    <p>This is some more original content</p>
  </my-component>
</div>
  • 描画された結果は以下の通り。
<div>
  <h1>I'm the parent title</h1>
  <div>
    <!-- slot タグの部分は削除されてる。 -->
    <h2>I'm the child title</h2>
    <p>This is some original content</p>
    <p>This is some more original content</p>
  </div>
</div>
  • 結論的には、子コンポーネントのelement内に、何も値が無い場合は、は破棄されない。何かある場合は、slotは破棄されて表示されない。

名前付きスロット

  • 要素は特別な属性 name を持ち、コンテンツを配信する方法をカスタマイズするために使用できます。

  • app-layout 以下のようなtemplateをもつコンポーネントがあると仮定する。

<!-- app-layoutのcomponetのtemplate -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
<app-layout>
  <h1 slot="header">Here might be a page title</h1>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <p slot="footer">Here's some contact info</p>
</app-layout>
  • 描画された結果は以下の通り。
<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>
  • slotに名前をつけると属性ごとに、slotの活用方法をカスタマイズできる。

スコープ付きスロット

<div class="child">
  <slot text="hello from child"></slot>
</div>
  • 親のtemplate。template tag内の、scopeを使って、propsに、子コンポーネントのslot内で設定したtextを渡す。
<div class="parent">
  <child>
    <template scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>
  • 出力は以下の通り。
<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>
<!-- リストコンポーネントの子のテンプレート -->
<ul>
  <slot name="item"
    v-for="item in items"
    :text="item.text">
    <!-- フォールバックコンテンツはここへ -->
  </slot>
</ul>
  • 親のtemplateは以下の通り。
<!-- itemsをitemsに -->
<my-awesome-list :items="items">
  <!-- scoped slot can be named too -->
  <template slot="item" scope="props">
    <li class="my-fancy-item">{{ props.text }}</li>
  </template>
</my-awesome-list>
  • フォールバックコンテンツってのは要するに、親コンポーネントに何も要素がなかったときに、表示されるコンテンツのことらしい。