やさしめ Vue.js チュートリアル(2) ~ データとイベントの制御

## はじめに

こちらは [やさしめ Vue.js チュートリアル(1) ~ Vue CLI でプロジェクト作成と Linter の設定](https://qiita.com/taiju59/items/7feac501bd24f0a50a7f) の続きとなります。

今回はデータとイベントの制御について学びます :muscle: 

## ハンズオン

### 1. 余計な記述の削除とタイトルの追加

まずは今回のハンズオン向けに初期状態の余計な記述を削除しましょう。

- `HelloWorld.vue` を削除
- `App.vue` 修正
  - タイトルを追加
  - HelloWorld コンポーネントを利用している箇所を削除
  - スタイルタグの中身を削除

以下のようになります。

```vue:App.vue
<template>
  <div id="app">
    <h1>Lovely Vue</h1>
  </div>
</template>

<script>
export default {
  name: "App"
};
</script>

<style></style>
```

:zap: 動作 :zap:
真っ白な背景にタイトルだけが表示されていれば OK です :ok_hand: 

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

### 2. 宣言的レンダリング

https://jp.vuejs.org/v2/guide/index.html#%E5%AE%A3%E8%A8%80%E7%9A%84%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0

スクリプト側で定義したデータをtemplate側で表示する方法を学びます。

まず、スクリプト側で `data` として `message` を定義

```diff:App.vue
  export default {
-   name: "App"
+   name: "App",
+   data: () => ({
+     message: "Spark joy!"
+   })
  };
```

テンプレート側で `{{ message }}` と記述して表示させます。

```diff:App.vue
  <div id="app">
    <h1>Lovely Vue</h1>
+   {{ message }}
  </div>
```

:zap: 動作 :zap:
「Lovely Vue」の下に「Spark joy!」と表示されていれば OK です :ok_hand: 

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

:bulb: ポイント :bulb:

- `data` で変数を宣言
  - ここでは詳しく説明しませんが、`data` は関数でなければなりません[^1]
- `{{ }}` で囲めば変数を展開できる

### 3. ユーザー入力の制御

https://jp.vuejs.org/v2/guide/index.html#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E5%85%A5%E5%8A%9B%E3%81%AE%E5%88%B6%E5%BE%A1

ユーザーの入力に応じて表示内容を変更してみましょう。

#### イベントリスナー登録(v-on)

テンプレート側にイベントリスナーを登録しましょう。
`input` 要素の `input` イベントが走ったときに `onInput` という名前の関数を呼ぶようにしておきます。

```diff:App.vue
  <div id="app">
    <h1>Lovely Vue</h1>
+   <input @input="onInput" />
+   <br />
    {{ message }}
  </div>
```

このままでは `onInput` という関数を呼ぼうとして存在せずエラーとなってしまうので、
スクリプト側に呼ばれるメソッドである `onInput` を定義しましょう。

この中で、入力内容を取得し `message` を上書きします。

```diff:App.vue
  export default {
    name: "App",
    data: () => ({
      message: "Spark joy!"
-   })
+   }),
+   methods: {
+     onInput(event) {
+       const text = event.target.value;
+       this.message = text;
+     }
+   }
};
```


:zap: 動作 :zap:
入力欄に入力した内容がその下に表示されるようになっていれば OK です :ok_hand: 

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

:bulb: ポイント :bulb:

- `@input` でインプットイベントと関数を紐付け[^2]
  - `@input` の `@` は生の JavaScript でいう `addEventListener` と考えておけば OK
- `methods` で関数を宣言
- スクリプト内では `this` のプロパティとして `data` にアクセスできる

#### 双方向バインディング(v-model)

画面表示時は入力欄の中は空なのに、
入力欄の下には「Spark joy!」が表示されていますね。

なぜこれが起きているのか？というと、
入力内容 => message という方向には連動していますが、
message => 入力内容 という方向には連動していないからです。

これを連動させるため、双方向バインディング(v-model)を用います。[^3]

テンプレート部分の `input` の記述を以下のように変更しましょう。

```diff:App.vue
- <input @input="onInput" />
+ <input v-model="message" />
```

いらなくなっちゃったので、スクリプト部分の `onInput` は削除しましょう。

```diff:App.vue
  export default {
    name: "App",
    data: () => ({
      message: "Spark joy!"
-   }),
+   })
-   methods: {
-     spark(event) {
-       const text = event.target.value;
-       this.message = text;
-     }
-   }
};
```

:zap: 動作 :zap:
表示時に入力欄に「Spark joy!」が入っていて、且つ、
入力内容に連動してその下の文字列が変化するはずです :ok_hand: 

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

:bulb: ポイント :bulb:

- `v-model` で入力内容と変数を双方向バインディングできる

### 4. ループ( `v-for` )

https://jp.vuejs.org/v2/guide/index.html#%E6%9D%A1%E4%BB%B6%E5%88%86%E5%B2%90%E3%81%A8%E3%83%AB%E3%83%BC%E3%83%97

いわゆる for 文です。
`v-for` を使うことでループでDOMを追加できます。

まず、スクリプト部分で `data` に `todos` という変数を定義します。

```diff:App.vue
  data: () => ({
+   todos: [
+     { id: 1, text: "ときめき" }
+   ],
    message: "Spark joy!"
  })
```

テンプレート部分で `v-for` を使ってループで表示させます。

```diff:App.vue
  <input v-model="message" />
  <br />
+ <ul>
+   <li v-for="todo in todos" :key="todo.id" class="task-card">
+     {{ todo.text }}
+   </li>
+ </ul>
```
`class` は普通に HTML 属性としての `class` になります。
スタイル部分にそれっぽいスタイルも追加しておきましょう。

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

:zap: 動作 :zap:
「ときめき」と書かれたカードっぽいやつが表示されているかと思います :ok_hand: 

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

さらに、タスクを追加できるようにしましょう。
テンプレート側に追加ボタンを記述します。

```diff:App.vue
  <input v-model="message" />
+ <button @click="add">追加</button>
  <br />
```

スクリプト側に `add` という関数を定義し、この中で新しいタスクを追加します。

```diff:App.vue
-   })
+   }),
+   methods: {
+     add() {
+       const newTodo = {
+         id: this.todos.length + 1,
+         text: this.message
+       };
+       this.todos.push(newTodo);
+       this.message = "";
+     }
+   }
  };
```

:zap: 動作 :zap:
「追加」ボタンを押すとタスクが追加さるようになっていれば OK です :ok_hand: 

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

:bulb: ポイント :bulb:

- `v-for` でテンプレート内で for 文が回せる
  - `key` 属性にはループ内で一意となる値を設定しましょう([スタイルガイド](https://jp.vuejs.org/v2/style-guide/index.html#%E3%82%AD%E3%83%BC%E4%BB%98%E3%81%8D-v-for-%E5%BF%85%E9%A0%88)参照)

### 5. 算出プロパティ( `computed` )

https://jp.vuejs.org/v2/guide/computed.html#%E7%AE%97%E5%87%BA%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3

気付いちゃった人もいるかもしれませんが、
このままでは入力欄が空でもタスクの追加ができちゃいます。

ダサいので、入力欄が空の場合は追加ボタンを無効化しましょう。

スクリプト部分に `computed` とその中に `isDisabled` という関数を定義します。

```diff:App.vue
    message: "Spark joy!"
  }),
+ computed: {
+   isDisabled() {
+     return this.message.length === 0;
+   }
+ },
  methods: {
```

`message` が空なら `true` を返します。
これをテンプレート部分で使います。

```diff:App.vue
- <button @click="add">追加</button>
+ <button :disabled="isDisabled" @click="add">追加</button>
```

これで、
入力内容が空の時は `isDisabled` が `true` => `disabled` が有効化、
入力内容がある時は `isDisabled` が `false` => `disabled` が無効化
します。

:zap: 動作 :zap:
入力欄が空の時に「追加」ボタンが押せなくなっていれば OK です :ok_hand: 

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

:bulb: ポイント :bulb:

- `computed` で算出プロパティを宣言
  - 定数や、既存の各変数から自動的に導き出される値を取得するために使います
  - 導出元の変数が変化すれば `computed` の結果も変わり、これに応じて DOM も変化します
- HTML 属性に値を指定する際、 `:disabled` など `:` を前に付けると右辺はスクリプトとして変数が展開されます[^4]

## おわりに

Vue の基本についてかいつまんで学びました。
ここまでやれば、小さいものなら何か作れるちゃうかも？と
思ってもらえたかもしれません。

また、リアクティブなフレームワークが初めての人は、
双方向バインディングや算出プロパティで **リアクティブの風** を感じてもらえたら嬉しいです。

次回はコンポーネントをファイル分割する方法について学びます。

次回 :point_right: [やさしめ Vue.js チュートリアル(3) ~ コンポーネントによる構成](https://qiita.com/taiju59/items/b54335674dca062fee3e)

Lovely Vue :heartbeat: 

[^1]: https://jp.vuejs.org/v2/guide/components.html#data-%E3%81%AF%E9%96%A2%E6%95%B0%E3%81%A7%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93
[^2]: [`@` は `v-on:` の省略記法](https://jp.vuejs.org/v2/guide/syntax.html#v-on-%E7%9C%81%E7%95%A5%E8%A8%98%E6%B3%95)
[^3]: 本質的な解決方法としてはちょっとずれているかもしれませんが、今回はv-modelの紹介が目的のためv-modelを使います
[^4]: これは [`v-bind:` の省略記法](https://jp.vuejs.org/v2/guide/syntax.html#v-bind-%E7%9C%81%E7%95%A5%E8%A8%98%E6%B3%95)です。