Vue.jsミニハンズオン（TODOリストをコンポーネント化する）

## Vue.jsミニハンズオン（TODOリストをコンポーネント化する）

AngularでもReactでもriot.jsでも満足できなかったひとに、ぴったりフィットなJSフレームワーク「Vue.js」のざっくりハンズオン第３回目です。

第１回目は「[Vue.jsミニハンズオン（TODOリスト作成）](https://qiita.com/moonglows76/items/358ef3cd1566c38ece3a)」をご覧ください。

このハンズオンではnode.jsのパッケージは使わず、Google ChromeとテキストエディタがあればOKです。

Vue.jsミニハンズオンのシリーズは以下を公開しています。

1. [Vue.jsミニハンズオン（TODOリスト作成）](https://qiita.com/moonglows76/items/358ef3cd1566c38ece3a)
1. [Vue.jsミニハンズオン（TODOリストにアニメーションをつける）](https://qiita.com/moonglows76/items/d550f0243319d7111ee3)
1. [Vue.jsミニハンズオン（TODOリストをコンポーネント化する）](https://qiita.com/moonglows76/items/242aaf265672797c6800)

## 今回の目標

目標は第2回目で作ったシンプルなTODOリストをコンポーネント化することです。

## コンポーネントって何だ？

そもそもコンポーネントってなんでしょう？
ざっくり言うとすれば「独自の機能をもたせた要素」というところです。
コンポーネントは外からの影響を受けないようにすることで再利用できる利点を持ちます。
iframe要素を使わなくても独立した世界を作れるってことですね。

### まずはグローバルなコンポーネントを作ってみる

TODOリストをコンポーネント化する方法がまだわからないので、
ひとまずコンポーネントについて学んでいきます。

まずはグローバルなコンポーネントの作り方です。
どのVueインスタンスからでも呼び出せます。

下の例では`my-component` というグローバルなコンポーネントを作って表示させています。

```html:component.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>コンポーネント</title>
</head>
<body>

<div id="example">
  <my-component></my-component>
</div>

<script src="https://unpkg.com/vue"></script>
<script src="./app_component.js"></script>

</body>
</html>
```

```JavaScript:app_component.js
Vue.component('my-component', {
  template: '<div>A global component!</div>'
})
// root インスタンスを作成する
const vm = new Vue({
  el: '#example',
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/zPaKaj)

`Vue.component` という命令でコンポーネントを作ることができます。
重要なのは `new Vue` でインスタンスを作る前に `Vue.component` があることです。
グローバルなコンポーネントは先にVueに登録しておかないとインスタンス内で利用できません。

### ローカルなコンポーネントを作ってみる

グローバルがあるならローカルな作り方もあります。

特定のVueインスタンス内でのみ利用するコンポーネントは `new Vue` でインスタンスを作るときに
 `components` オプションを利用します。

下記の例では `my-component` をローカルに作っています。

```js
var Child = {
  template: '<div>A local component!</div>'
}
const vm = new Vue({
  el: '#example',
  components: {
    'my-component': Child
  }
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/eeKdjy)

この作り方では `components` オプションを使って作成したインスタンス内でのみ利用可能なコンポーネントを作ることができます。

### コンポーネント内のデータは関数でなければならない

`new Vue` でインスタンスを作り出すときに指定するほどんどのオプションはコンポーネントでも利用できますが、`data` オプションについては関数でなければならないというルールがあります。
テストで下のようにグローバルなコンポーネントを作ってみます。

```js
Vue.component('my-component', {
  '<div>{{ message }}, global component!</div>',
  data: {
    message: 'Hello'
  }
})
```

データが表示されず、JavaScriptコンソールに「データは関数にしてね」とエラーが表示されます。
正しく表示させるには下記のように関数にします。

```js
Vue.component('my-component', {
  template:   '<div>{{ message }}, global component!</div>',
  data: function(){
    return {
      message: 'hello'
    }
  }
})
```
[CODEPEN](https://codepen.io/moonglows76/pen/jaKMJw)

ちなみに、`return` のあとに `{}` 使わずにデータを参照する例を作ってみます。

```html:component.html
<div id="example-2">
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
</div>
```

```JavaScript:app_component.js
var data = { counter: 0 }
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  data: function () {
    return data
  }
})
new Vue({
  el: '#example-2'
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/ZaRpgo)

表示されたボタンをクリックすると数が1ずつ増えますが、ボタンごとではなく、全部のボタンの数が増えてしまいます。
ボタンごとに数が増えるようにするためには、`{}` を加えて、それぞれのボタンが未使用のデータを持つようにします。

```JavaScript:app_component.js
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  }
})
const vm2 = new Vue({
  el: '#example-2'
})
```

こうするとボタンごとに数が増えるようにすることができます。


## 親子でのデータのやり取り

さて、これまではコンポーネントの作り方をみてきました。
次に親コンポーネントと子コンポーネントのデータのやりとりをみていきます。

Vueでは親子コンポーネントの関係は、親から子へは**props down**（プロパティでデータを伝える）, 子から親へは**event up**（イベントを使って親に伝える）となります。

- 親→子：**props down**（プロパティでデータを伝える）
- 子→親：**event up**（イベントを使って親に伝える）

Vueでは**データは親から子に一方的に流す**というルールを作ってあり、その逆はありません。
こうすることで複数の子が偶然親の状態を変更してしまうことを防いでいます。

さて、まずは親→子のデータの伝達を見てみましょう。

## 親→子：props down（プロパティでデータを伝える）

親からプロパティを使って渡すのでカスタム要素にプロパティを記述します。

```html:component.html
<div id="example-3">
  <child message="hello!"></child>
</div>
```

子コンポーネント側では `props` プロパティで親から受け取るプロパティを指定します。

```JavaScript:app_component.js
Vue.component('child', {
  props: ['message'],
  template: '<span>{{ message }}</span>'
})

const vm3 = new Vue({
  el: '#example-3'
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/RjJXjd)

もしJavaScript側で `myMessage` のようにプロパティ名を付ける場合は、HTML側では `my-message` のようにハイフン区切りのプロパティ名にする必要があります。これはHTMLが大文字・小文字の区別がないためです。気をつけましょう。

### プロパティ1つのデータバインディングしてみる

親から `v-bind` ディレクティブを使ってデータバインディングする渡し方もできます。

```html:component.html
<div id="example-4">
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>
```

子コンポーネント側では `v-bind` ディレクティブに指定したプロパティ名を指定します。
親コンポーネント側では `data` オプションあるにデータを用意します。

```JavaScript:app_component.js
Vue.component('child', {
  props: ['myMessage'],
  template: '<span>{{myMessage}}</span>'
})

const vm4 = new Vue({
  el: '#example-4',
  data: {
    parentMsg: 'Message from parent'
  }
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/BmVXMo)

### オブジェクトのデータバインディングしてみる

子コンポーネントにオブジェクトを渡すこともできます。
この場合は `v-bind:プロパティ名` ではなく、 `v-bind` としてオブジェクトを値に指定します。

```html:component.html
<div id="example-5">
  <todo-item v-bind="todos"></todo-item>
</div>
```

子コンポーネント側では `props` プロパティにオブジェクト内のプロパティ名をちゃんと指定します。

```JavaScript:app_component.js
Vue.component('todo-item', {
  props: ['text','isComplete'],
  template: '<div>{{text}} - {{isComplete}}</div>'
})

const vm5 = new Vue({
  el: '#example-5',
  data: {
    todos: {
      text: 'Learn Vue',
      isComplete: false
    }
  }
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/yPEmdo)


### オブジェクトを使ってリストを表示してみる

親コンポーネントから渡されたオブジェクトでリストを表示してみます。
HTMLではリスト表示のために `v-for` ディレクティブを利用し、 `v-bind` ディレクティブでは `todos`に替わって `todo` を渡します。

```html:component.html
<div id="example-5">
  <todo-item v-for="todo in todos" v-bind="todo"></todo-item>
</div>
```

子コンポーネント側ではデータ `todos` にデータを複数用意すれば準備OK。

```JavaScript:app_component.js
Vue.component('todo-item', {
  props: ['text','isComplete'],
  template: '<div>{{text}} - {{isComplete}}</div>'
})

const vm5 = new Vue({
  el: '#example-5',
  data: {
    todos: [
      {
        text: 'Learn Vue',
        isComplete: false
      },
      {
        text: 'Learn Vue2',
        isComplete: true
      },
    ]
  }
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/vWaBBo)

ちゃんとリストが表示されましたね。

### プロパティを使って渡すときの注意点

プロパティを使って親から子に数値を渡すとき、下記のように渡すと実は数値ではなく文字列になってしまいます。
これはHTMLの属性値にある数値をJavaScriptで扱うときに一般的に出てくる問題です。

```HTML
<comp some-prop="1"></comp>
```

Vueでは `v-bind` ディレクティブを使って指定する値にJavaScriptの式を指定できます。
つまりHTMLの属性値とは違って文字列ではなく数値として扱うことができます。

```HTML
<comp v-bind:some-prop="1"></comp>
```

## 子→親：event up（イベントを使って親に伝える）

さて、ここまで親→子のデータ伝達を見てきました。今度は子→親のデータ伝達です。
ただ、Vueには**データは親から子に一方的に流す**というルールがあり、**子から親のデータを直接編集することはできません**。ここで使うのがVueのカスタムイベントです。

### カスタムイベント

子から親を編集するときは下記の流れになります。

- **v-on:イベント名** で子で発生したイベントを拾う設定をする
- 子の関数内で **this.$emit('イベント名')** を指定して親に対してイベントを起こす

ひとまずソースを触ってみましょう。
下記はボタン（子）をクリックすると親の数字が加算されるサンプルです。

```html:component.html
<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
```

親に `p` 要素で `total` というデータを表示する部分が作ってあります。
`button-counter` 要素に `v-on` ディレクティブで設定しているイベントが `increment` ？
聞いたことのないイベント名ですね。これがカスタムイベントです。

まあなんにしろこれで `increment` というイベントが起こったときに `incrementTotal` という関数を実行されることになります。

```JavaScript:app_component.js
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
const vmcounter = new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})
```

[CODEPEN](https://codepen.io/moonglows76/pen/eejOjb)

次にJavaScriptで親のコンポーネントを見ます。

- `methods` オプションに関数 `incrementTotal` が指定されていて、`total` というデータに+1する
- `incrementTotal` はHTMLファイルで書かれていた関数である

次に子コンポーネントを見ます。

- `template` オプションを見ると `button-counter` 要素は `button` 要素であり、 `click` イベントが仕込まれていて、関数 `incrementCounter` を呼び出している
- `counter` というデータがボタンごとに存在するように `return {}` の中に指定されていて、`template` オプションの中でそのデータを表示している
- `methods` オプションに関数 `incrementCounter` が指定されていて `counter` というデータに+1する
- **`this.$emit('increment')` で親の `increment` イベントを起こす**
  - **`this.$emit('increment', データ)` の形で子から親にデータを渡すことが可能**

**子から親に対してカスタムイベントを起こして親の関数を動作させ、親の関数を使って親のデータを編集する**ということになります。

まとめると、重要なのは下記になります。

- **親のデータは親が、子のデータは子が編集**
- **親から子にデータを渡す仕組みがある**
- **イベントは子から親に伝える仕組みがある**

## TODOリストをコンポーネント化してみる

さて、目的の「TODOリストをコンポーネント化」に取り掛かりましょう。

今回コンポーネント化するのはリスト部分のみです。
見た目は変わらないんですが、親子コンポーネントのやりとりの感覚をつかむのにはいいと思います。

まずはHTML。編集するところだけ抜き出します。

```html:todo.html
  <transition-group name="list-complete" tag="ul">
    <li v-for="item in items" v-bind:key="item" class="list-complete-item">
      <label v-bind:class="{ done: item.isChecked }">
        <input type="checkbox" v-model="item.isChecked"> {{ item.title }}
      </label>
    </li>
  </transition-group>
```

↓　`li` 要素をコンポーネント化

```html:todo.html
  <transition-group name="list-complete" tag="ul">
    <todo-item v-for="item in items" v-bind="item" v-bind:key="item.id" v-on:delete="updateCheck"></todo-item>
  </transition-group>
```

修正点は以下の通り。

* コンポーネント名は `todo-item` です。
* コンポーネントを `v-for` ディレクティブで配列内の項目分作ります。
* コンポーネント内に表示するデータを `v-bind` でオブジェクト `item` を渡しています。
* `v-for` ディレクティブは `v-bind:key` を指定する必要があるのでここでは配列内に `id` というプロパティを追加して指定しました（前回は `item` を指定していましたが「StringかNumberでないとダメ」とコンソールにエラーが出ていたので修正しました）。
* カスタムイベントとして `v-on:delete` に親コンポーネントの関数 `updateCheck` を指定しました。

そしてJavaScriptの調整に入ります。
※データ構成が替わってしまっているので、動作確認をする前に前回までのlocalStorageのデータは削除してください（すみません…）。

まずは子コンポーネント `todo-item` をグローバルなコンポーネントとして追加。

```JavaScript:todo.js
//Vue.componentをまるっと追加
Vue.component('todo-item', {
  props: ['title','isChecked','id'],
  template: `
  <li class="list-complete-item">
    <label v-bind:class="{ done: isChecked }">
      <input type="checkbox" v-model="childisChecked" v-on:change="deleteCheck"> {{ title }}
    </label>
  </li>`,
  data: function(){
    return {
      childisChecked: this.isChecked  //v-modelでisCheckedを指定できないため追加
    }
  },
  methods: {
    deleteCheck: function(){
      this.$emit('delete', this.childisChecked, this.id);
    }
  }
})
```

`template` オプションの `li` 要素内の記述は上で表示していた修正前の `li` 要素の記述とちょこっと変わってます。

* `label` 要素で指定する `isChecked` の頭についていた `item.` は削除 → `props` オプションで `'title','isChecked','id'` をそれぞれ参照するように指定したため。
* `input` 要素の `v-model` ディレクティブの値を子コンポーネントの `data` オプションで設定した `childisChecked` に変更　→　親コンポーネントのデータを直接編集できないため
* 親コンポーネントにチェックボックスの値の変更を知らせるため、`v-on:change` でイベントを拾い、子コンポーネント内の `methods` オプションに設定した関数 `deleteCheck`を呼び出す
* 関数 `deleteCheck` で `$emit` を利用して、親の `v-on:delete` を動作させ、引数で値を渡す（ここではチェックボックスの状態とidを渡しています）

そして親コンポーネントの修正。

```JavaScript:todo.js
const vm = new Vue({
  el: '#app',
  data: {
    items: [],
    newItemTitle: '',
  },
  methods: {
    updateCheck: function(childChecked, childIndex){  //updateCheckメソッドをまるっと追加
      for (let i = 0; i < this.items.length; i++) {
        if (this.items[i].id === childIndex) {
          this.items[i].isChecked = childChecked;
          break;
        }
      }
      this.saveTodo();
    },
    addTodo: function(newTitle){
      let date = Date.now(); //idに利用する数値
      this.items.push({
        title: newTitle,
        isChecked: false,
        id: date  //idを追加
      });
      this.newItemTitle = '';
      this.saveTodo();
    },
    deleteTodo: function(){
      this.items = this.items.filter(function (item) {
        return item.isChecked === false;
      });
      this.saveTodo();
    },
    saveTodo: function(){
      localStorage.setItem('items', JSON.stringify(this.items));
    },
    loadTodo: function(){
      this.items = JSON.parse( localStorage.getItem('items') );
      if( !this.items ){
        this.items = [];
      }
    },
  },
  mounted: function(){
    this.loadTodo();
  },
})
```

修正点は以下の通り。

* `methods` オプションに関数 `updateCheck` を追加して、子コンポーネントのTODO項目のチェック状態を親データに反映する処理を追加
* `methods` オプション内の関数 `addTodo` で `id` プロパティを追加

[CODEPEN](https://codepen.io/moonglows76/pen/wPXLVZ)

まあなんとも面倒な処理ですが、親と子のそれぞれのデータが分けられているのが分かってもらえるのではないでしょうか。



## Vue.js公式のコンポーネントのガイドはかなりボリュームがあります

Vue.js公式のコンポーネントのガイドはについては下記をご覧ください。
このハンズオンの例の大半はこのガイドから持ってきました。
そしてかなりはしょっています。しっかり学びたい方はこちらでも学んでみてください。
とても良くまとまっていて勉強しやすいですよ。

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