やさしめ Vue.js チュートリアル(3) ~ コンポーネントによる構成

## はじめに

こちらは [やさしめ Vue.js チュートリアル(2) ~ データとイベントの制御](https://qiita.com/taiju59/items/a45908ea1cb6c5199130) の続きです。

今までは表示内容を全て `App.vue` の1ファイルに詰め込んでました。
このまま `App.vue` を拡張していくとカオスになるので、
コンポーネント分割の方法を学びます :muscle: 

## ハンズオン

### 1. コンポーネント化(components, props)

では、カード部分をコンポーネントとして切り出してみましょう。

`src/components` 内に、新たに `TaskCard.vue` を追加します。[^1][^2]

```vue:TaskCard.vue
<template>
  <li class="task-card">
    {{ text }}
  </li>
</template>

<script>
export default {
  name: "TaskCard",
  props: {
    text: {
      type: String,
      default: ""
    }
  }
};
</script>

<style>
.task-card {
  border: solid 1px;
  margin: 5px;
  padding: 2px;
  width: 250px;
}
</style>
```

このコンポーネントを `App.vue` で使うよう修正します。

不要になったスタイル部分を削除

```diff:App.vue
- <style>
- .task-card {
-   border: solid 1px;
-   margin: 5px;
-   padding: 2px;
-   width: 250px;
- }
- </style>
+ <style></style>
```

スクリプト部分でコンポーネント登録

```diff:App.vue
  <script>
+ import TaskCard from "@/components/TaskCard.vue";
+
  export default {
    name: "App",
+   components: {
+     TaskCard
+   },
    computed: {
```

テンプレート部分でコンポーネントを使うよう修正

```diff:App.vue
-  <li v-for="todo in todos" :key="todo.id" class="task-card">
-    {{ todo.text }}
-  </li>
+  <TaskCard v-for="todo in todos" :key="todo.id" :text="todo.text" />
```

:zap: 動作 :zap:
修正前と同じように、タスクの追加ができていれば OK です :ok_hand: 

:book: ソースコード :book:
https://github.com/taiju59/lovely-vue/commit/120236e09bfb2af459b422e134e76b47ebfb76aa

:bulb: ポイント :bulb:

- コンポーネントを `.vue` ファイルとして切り出すことができる
  1. `.vue` ファイル作成
  2. 親コンポーネントで `import`
      - Vue CLI ではパス文字列の `@` は `src` ディレクトリのエイリアスとなります
  3. `components` でコンポーネント登録
  4. テンプレート部分で登録したコンポーネントを使う
- `props` でプロパティを定義
- 親コンポーネントから通常のHTML属性と同様に `:hoge` でプロパティに値を渡せる[^v-bind]

### 2. 親コンポーネントへのイベント伝搬(emit)

親コンポーネントへのイベント伝搬の方法について学ぶため、
タスクの削除機能を実装してみましょう。

いくつかの段階を踏んで実装していきます。

#### 削除ボタンの設置

まず、タスクの削除ボタンを設置します。

テンプレート部分

```diff:TaskCard.vue
<template>
  <li class="task-card">
-     {{ text }}
+     <span>{{ text }}</span>
+     <button @click="remove">削除</button>
  </li>
</template>
```

スクリプト部分

```diff:TaskCard.vue
      text: {
        type: String,
        default: ""
      }
+   },
+   methods: {
+     remove() {
+       // TODO: 削除機能の実装
+       console.log("remove");
+     }
    }
  }
};
</script>
```

スタイル部分

```diff:TaskCard.vue
    word-break: break-all;
  }
+ .task-card > button {
+   float: right;
+ }
</style>
```

:zap: 動作 :zap:
カードに削除ボタンが表示されたはずです。
削除ボタンを押すとコンソールに「remove」と出力されれば OK です :ok_hand: 

:book: ソースコード :book:
https://github.com/taiju59/lovely-vue/commit/1409782f285c7b0427e1651beb07a29b8a3582c4

#### 削除するカードの判別

「remove」と表示されるだけではどのカードか判別できませんね。
`App` から `TaskCard` へ `id` を渡すよう修正します。[^3]

まずは `TaskCard` のプロパティに `id` を定義します。

```diff:TaskCard.vue
  props: {
+   id: {
+     type: Number,
+     required: true
+   },
    text: {
...
    remove() {
      // TODO: 削除機能の実装
-     console.log("remove");
+     console.log(`remove: ${this.id}`);
```

`App` から `TaskCard` へ `id` を渡します。

```diff:App.vue
- <TaskCard v-for="todo in todos" :key="todo.id" :text="todo.text" />
+ <TaskCard
+   v-for="todo in todos"
+   :id="todo.id"
+   :key="todo.id"
+   :text="todo.text"
+ />
```

(自動整形の都合で改行されただけで `:id="todo.id"` を追加しただけです)

:zap: 動作 :zap:
さて、これで削除ボタンを押せば、「remove: 1」のように
削除したいタスクの `id` まで出力されるようになったはずです :ok_hand: 

:book: ソースコード :book:
https://github.com/taiju59/lovely-vue/commit/71b9126a7d7348dae46cc4b6d28494f1a431beb2

#### emitを使ってタスクの削除

子コンポーネントで発生したイベントをどうやって親コンポーネントに伝えるのか？
ここで使うのが `emit` です。

`TaskCard.vue` から `emit` で親コンポーネントへイベントを伝えます。

```diff:TaskCard.vue
  remove() {
-   // TODO: 削除機能の実装
    console.log(`remove ${this.id}`);
+   this.$emit("remove-task", this.id);
  }
```

`App.vue` でこれを受け取ります。

```diff:App.vue
  <TaskCard
    v-for="todo in todos"
    :id="todo.id"
    :key="todo.id"
    :text="todo.text"
+   @remove-task="remove"
  />
```

また、実際に `todo` 削除する関数を `methods` に追加します。

```diff:App.vue
  methods: {
    ...
+   remove(id) {
+     this.todos = this.todos.filter(todo => todo.id !== id);
+   }
```

(特定 `id` を持つ `todo` 以外の配列を再生成することで実質的に削除しています)

:zap: 動作 :zap:
さて、これでようやくタスクが削除されるようになったかと思います :ok_hand: 

:book: ソースコード :book:
https://github.com/taiju59/lovely-vue/commit/a6e0ea5c27c95cb3a7f3aacecd6f2943a14d5795

:bulb: ポイント :bulb:

- `emit` で親コンポーネントへイベント伝搬
  - 第一引数にイベント名、第二引数以降に引数を渡します(https://jp.vuejs.org/v2/api/#vm-emit)
- `emit` されたイベントは、親コンポーネントでデフォルトのイベントのように  `v-on:hoge` または `@hoge` で受け取れる

### おまけ(v-if)

参考: https://jp.vuejs.org/v2/guide/conditional.html#v-if

`v-if` を使ってタスクが1つもないときは「タスクはありません」と表示させます。

```diff:App.vue
- <ul>
-   <TaskCard
-     v-for="todo in todos"
-     :id="todo.id"
-     :key="todo.id"
-     :text="todo.text"
-     @remove-task="remove"
-   />
- </ul>
+ <template v-if="todos.length === 0">
+   タスクはありません
+ </template>
+ <template v-else>
+ <ul>
+   <TaskCard
+     v-for="todo in todos"
+     :id="todo.id"
+     :key="todo.id"
+     :text="todo.text"
+     @remove-task="remove"
+     />
+   </ul>
+ </template>
```

:zap: 動作 :zap:
全てのタスクを削除すると「タスクはありません」と画面上に表示されれば OK です :ok_hand: 

:book: ソースコード :book:
https://github.com/taiju59/lovely-vue/commit/b65fec24cbd707771c947a69b69138d205e1b5a6?diff=unified

:bulb: ポイント :bulb:

- `v-if` で条件を満たす場合のみの描画が可能
  - `v-else-id`、`v-else-if` もあります

## おわりに

今回はコンポーネントによる構成を学びました。

これでアプリケーションが大きくなってもファイルを分割することで
なんとかまあカオスにならずにやっていけるんじゃないかなと思います。

ここまでで Vue.js の記法としては一旦終わりですが、
まだまだ伝えられていないことは多いです。

作る前にもう少し何かを読みたい人はとにかく公式ドキュメントを読むのがオススメです。
このチュートリアルをやった人であれば、サクサクと読めるんじゃないかと信じています :thumbsup: 
https://jp.vuejs.org/v2/guide

次回は Vue.js そのものから少し離れて、知らないままでも作れるけど
当たり前に知っておきたいよね、みたいな周辺知識を紹介できればと思います

次回 :point_right: [やさしめ Vue.js チュートリアル(4) ~ 周辺ツールや情報紹介](https://qiita.com/taiju59/items/c13865c9998502cb95dc)

Lovely Vue :heartbeat: 

[^1]: コンポーネント名はスタイルガイドで複数単語の命名を[必須レベルに推奨されている](https://jp.vuejs.org/v2/style-guide/index.html#%E8%A4%87%E6%95%B0%E5%8D%98%E8%AA%9E%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E5%90%8D-%E5%BF%85%E9%A0%88)
[^2]: スタイルに `scoped` 属性を付与することで CSS のスコープをこのコンポーネント内に限定することができます(https://jp.vuejs.org/v2/guide/comparison.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97-CSS%EF%BC%88Scoped-CSS%EF%BC%89)
[^v-bind]: [`:` は `v-bind:` の省略記法](https://jp.vuejs.org/v2/guide/syntax.html#v-bind-%E7%9C%81%E7%95%A5%E8%A8%98%E6%B3%95)
[^3]: `todo` ごと渡した方が綺麗かもしれません。今回はわかりやすさ重視で分けて渡します