[Vue.js 2.x] 非コンポーネントvsコンポーネントでの登録フォームの実装

# 前提
Vue.jsは使っているが、コンポーネントを使った実装をまだしていない人、少しし始めた人向けです。

## この記事のねらい
コンポーネント使わずとも、Vueインスタンスを作成し、`data`オブジェクトに必要なプロパティを追加していって、、1つのJSファイルで問題なく実装できるよね。
コンポーネントする意味って何なの？(´・ω・｀)という状態から、この記事を読んで貰った後には、**コンポーネント化したほうがよさそう！**と思ってもらえば幸いです。

# コンポーネント化するメリット
### メンテナンスしやすい 
追加修正が発生した場合に**該当箇所が見つけやすい**。
必要なファイルのみ修正を行えばよい。
**ロジック部分、非ロジックのHTML部分で切り分け**ができる
 
### 複数人で作業する場合、分業しやすい
同時に作業しても修正箇所がかぶらないので**コンフリクトの発生が避けられる**。

### 可読性が高い
Vue.jsを使った実装はコンポーネントを使ったものが一般的と思うので、パターンを掴むと読み解きやすい。
[スタイルガイド](https://jp.vuejs.org/v2/style-guide/)に沿った実装だと更に読みやすい。


# メリットを実感するために実装してみる
とは言ってもふ～んという感じで実際に使ってみないとよくわかりません。
コンポーネントのよいとこを実感するために、簡単なサンプルアプリを2つのアプローチで作っていきます。
**コンポーネントを使わない実装**と、**コンポーネントを使った実装**をしてみます。

サンプルは、一般的なWEB上の登録フォームにします。
コンポーネントの有用性を検証するため、登録フォームの内容は選択式の質問でなく、テキスト入力するものを主に使います。

こうゆうの

![スクリーンショット 2017-12-11 13.24.49.png](https://qiita-image-store.s3.amazonaws.com/0/111637/a8c693c7-dc9e-5d9c-1bfb-84ff3b544d43.png)

## サンプルアプリの要件
これらが入力出来るフォームを作ります。

| 項目名       | 入力形式  |  入力例 |
|:-------------|:---------|:------------------|
| 姓           |               |                |
| 名           |               |              |
| フリガナ（セイ）  | 全角カタカナ    |                |
| フリガナ（メイ）  | 全角カタカナ    |                  |
| 郵便番号１     | ３桁の半角数字  |              |
| 郵便番号２     | 4桁の半角数字  |             |
| 都道府県      |               | 例：東京都             |
| 住所１        |                | 例：中央区新川1-2-3            |
| 住所2        |               | 例：Xビル５F            |


- 入力形式はすべてテキストフィールド
- このサンプルではデータベースへの登録は行いません
- 各項目は**全部必須項目**という前提

Vueに関する処理

- 入力のバリデーションが**各項目別**に行える
- 各項目は全部必須項目という前提なので、**全部の項目が入り、バリデーションをクリアしている場合にのみ登録ボタンが押せる**ようになる

では、実装してきましょう！

## サンプルアプリ
[こちら](https://github.com/kaorina/vue-js-components)

# 1. コンポーネントを使わずに実装する

## 環境について
Ruby on Rails5系でベースの環境を用意し、`http://localhost:3000/registrations` でアクセスできるページを作りました。

JavaScriptファイルの構成

```
./app/assets/javascripts/
├── application.js
├── cable.js
├── channels
├── registrations
│   ├── form.js # コンポーネントなしver.用のJavaScriptファイル
└── vue.js      # 公式サイトのソースを配置
```

Viewの構成

```
./app/views/registrations/
├── _form.html.erb # コンポーネントなしver.用のパーシャル
└── index.html.erb
```

## 登録フォーム画面を描画
input要素を置いて、入力フォームを用意します。

```html:app/views/registrations/_form.html.erb
<div id="registrations" class="container">
  <h3>登録フォーム(コンポーネント利用なしver.)</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <input type="text" class="form-control" placeholder="田中">
      <input type="text" class="form-control" placeholder="太郎">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <input type="text" class="form-control" placeholder="タナカ">
      <input type="text" class="form-control" placeholder="タロウ">
      <span class="text-danger">正しい形式で入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input type="text" class="form-control" placeholder="111">
      -
      <input type="text" class="form-control" placeholder="2222">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <input type="text" class="form-control" placeholder="東京都">
    <input type="text" class="form-control" placeholder="中央区新川1-2-3">
    <span>マンション・ビル名</span>
    <input type="text" class="form-control" placeholder="Xビル５F">
    <span class="text-danger">全ての住所欄を入力してください</span>
  </div>

  <button type="button" class="btn btn-info">Submit</button>
</div>
```

現時点でこのような画面が描画されています

![スクリーンショット 2017-12-06 13.24.25.png](https://qiita-image-store.s3.amazonaws.com/0/111637/9e31f5b0-5a52-314d-07d5-0a9a9f47ccf7.png)

## Vueインスタンスを作成し、入力値を取得
`data`オブジェクトに、各項目名のプロパティを定義します。
今回オブジェクトを用意し、名前に関する情報は`user`、住所に関するデータは`address`オブジェクトに格納するように定義しました。

```js:app/assets/javascripts/registrations/form.js
$(function () {
  new Vue({
    el: '#registrations',
    data: {
      user: {
        familyName: '',
        firstName: '',
        familyNameKana: '',
        firstNameKana: ''
      },
      address: {
        zipCode3digit: '',
        zipCode4digit: '',
        prefecture: '',
        city: '',
        buildingName: ''
      }
    }
  });
});
```

それに合わせて、HTML側に`v-model`を追加します。これにより、各項目の入力値が参照できるようになりました。

```html:app/views/registrations/_form.html.erb
<div id="registrations" class="container">
  <h3>登録フォーム(コンポーネント利用なしver.)</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <input v-model="user.familyName" type="text" class="form-control" placeholder="田中">
      <input v-model="user.firstName" type="text" class="form-control" placeholder="太郎">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <input v-model="user.familyNameKana" type="text" class="form-control" placeholder="タナカ">
      <input v-model="user.firstNameKana" type="text" class="form-control" placeholder="タロウ">
      <span class="text-danger">正しい形式で入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input v-model="address.zipCode3digit" type="text" class="form-control" placeholder="111">
      -
      <input v-model="address.zipCode4digit" type="text" class="form-control" placeholder="2222">
      <span class="text-danger">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <input v-model="address.prefecture" type="text" class="form-control" placeholder="東京都">
    <input v-model="address.city" type="text" class="form-control" placeholder="中央区新川1-2-3">
    <span>マンション・ビル名</span>
    <input v-model="address.buildingName" type="text" class="form-control" placeholder="Xビル５F">
    <span class="text-danger">全ての住所欄を入力してください</span>
  </div>

  <button type="button" class="btn btn-info">Submit</button>
</div>
```

Vue.jsのdevtoolsで確認するとオブジェクトの中身が確認できました。
![スクリーンショット 2017-12-11 13.32.17.png](https://qiita-image-store.s3.amazonaws.com/0/111637/86bdc192-d2ee-1bd4-7698-5b13f8536164.png)


## 各項目のバリデーションを実装する

下記の要件に着手していきます。

```
- 入力のバリデーションが各項目別に行える
- 各項目は全部必須項目という前提なので、全部の項目が入り、バリデーションをクリアしている場合にのみ登録ボタンが押せるようになる
```

### バリデーション実行の概要図
![スクリーンショット 2017-12-12 15.12.59.png](https://qiita-image-store.s3.amazonaws.com/0/111637/3c0b4050-ec47-192c-a1ef-1bf4318d0ea5.png)

まず、各項目のバリデーションを行えるようにしていきます。

[算出プロパティ computed](https://jp.vuejs.org/v2/guide/computed.html)を使い、入力値が変わる度にバリデーションを実行するようにします。

```js:app/assets/javascripts/registrations/form.js
$(function () {
  new Vue({
    el: '#registrations',
    data: {
      user: {
        familyName: '',
        firstName: '',
        familyNameKana: '',
        firstNameKana: ''
      },
      address: {
        zipCode3digit: '',
        zipCode4digit: '',
        prefecture: '',
        city: '',
        buildingName: ''
      }
    },
    computed: {
      validation: function () {
        var user = this.user;
        var address = this.address;
        var kanaPattern = /^[\u30a0-\u30ff]+$/;
        return {
          name: user.familyName.trim() && user.firstName.trim(),
          furigana: kanaPattern.test(user.familyNameKana.trim()) && kanaPattern.test(user.firstNameKana.trim()),
          zipCode: this.validationZipcode,
          address: address.prefecture.trim() && address.city.trim() && address.buildingName.trim()
        };
      },
      validationZipcode: function () {
        var zipCode3 = this.address.zipCode3digit.trim();
        var zipCode4 = this.address.zipCode4digit.trim();
        var p = /^\d+$/;
        return (p.test(zipCode3) && p.test(zipCode4)) && (zipCode3.length === 3 && zipCode4.length === 4);
      }
    }
  });
});
```
computedの `validation`でバリデーションを行っています。

| 項目名  | 検証内容                                                                     |
|:-------|:-----------------------------------------------------------------------------|
| 姓名    |    入力値があればよいので、入力された値からtrimメソッドで前後の空白を省いた値を返します。           |
| 住所    |    同上           |
| フリガナ  |    正規表現の全角カタカナパターンを使い、testメソッドで検証。その結果（true/false）を返します。           |
| 郵便番号 |    バリデーションが少し複雑だったので、`validationZipcode`という別のcomputed値にし、半角数字であること＆桁数を検証しています。 | 


この`validation`を実行した結果によって、HTML側でエラーメッセージの表示を`v-show`を使って制御します。

```html:app/views/registrations/_form.html.erb
<div id="registrations" class="container">
  <h3>登録フォーム(コンポーネント利用なしver.)</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <input v-model="user.familyName" type="text" class="form-control" placeholder="田中">
      <input v-model="user.firstName" type="text" class="form-control" placeholder="太郎">
      <span class="text-danger" v-show="!validation.name">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <input v-model="user.familyNameKana" type="text" class="form-control" placeholder="タナカ">
      <input v-model="user.firstNameKana" type="text" class="form-control" placeholder="タロウ">
      <span class="text-danger" v-show="!validation.furigana">正しい形式で入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input v-model="address.zipCode3digit" type="text" class="form-control" placeholder="111">
      -
      <input v-model="address.zipCode4digit" type="text" class="form-control" placeholder="2222">
      <span class="text-danger" v-show="!validation.zipCode">入力してください</span>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <input v-model="address.prefecture" type="text" class="form-control" placeholder="東京都">
    <input v-model="address.city" type="text" class="form-control" placeholder="中央区新川1-2-3">
    <span>マンション・ビル名</span>
    <input v-model="address.buildingName" type="text" class="form-control" placeholder="Xビル５F">
    <span class="text-danger" v-show="!validation.address">全ての住所欄を入力してください</span>
  </div>

  <button type="button" class="btn btn-info">Submit</button>
</div>
```

これでバリデーションエラーの場合のみメッセージが表示されるようになりました。

## 全てのバリデーションがOKの場合のみ登録ボタンが押せるようにする

全項目のバリデーション結果を取得し、検証するためにcomputed値`isValid`を定義する。
JavaScriptの`every`メソッドを使って`validation`の返り値の**全ての値**がtrueであることを検証する。

```js:app/assets/javascripts/registrations/form.js
$(function () {
  new Vue({
    el: '#registrations',
    data: {
      user: {
        familyName: '',
        firstName: '',
        familyNameKana: '',
        firstNameKana: ''
      },
      address: {
        zipCode3digit: '',
        zipCode4digit: '',
        prefecture: '',
        city: '',
        buildingName: ''
      }
    },
    computed: {
      isValid: function () {
        var validation = this.validation;
        return Object.keys(validation).every(function (key) {
          return validation[key];
        });
      },
      validation: function () {
        var user = this.user;
        var address = this.address;
        var kanaPattern = /^[\u30a0-\u30ff]+$/;
        return {
          name: user.familyName.trim() && user.firstName.trim(),
          furigana: kanaPattern.test(user.familyNameKana.trim()) && kanaPattern.test(user.firstNameKana.trim()),
          zipCode: this.validationZipcode,
          address: address.prefecture.trim() && address.city.trim() && address.buildingName.trim()
        };
      },
      validationZipcode: function () {
        var zipCode3 = this.address.zipCode3digit.trim();
        var zipCode4 = this.address.zipCode4digit.trim();
        var p = /^\d+$/;
        return (p.test(zipCode3) && p.test(zipCode4)) && (zipCode3.length === 3 && zipCode4.length === 4);
      }
    }
  });
});
```

HTML側では`isValid`の結果を元に、登録ボタンにclassを付与するかどうかを制御する。[Bootstrapのbuttonをdisabledの状態にする](https://v4-alpha.getbootstrap.com/components/buttons/#disabled-state)

```html
<!-- ボタン部分のみ抜粋 -->

  <button type="button" :disabled="!isValid" class="btn btn-info">Submit</button>
```

ここまで実装すると、全てのバリデーションをクリアした場合のみ登録ボタンが押下できるようになりました！

# ここまでの感想
実装したコードを眺めてみてどうでしょうか。
Vueインスタンスの`data`オブジェクトが中々のボリュームになっています！
**実際の登録画面を想定すると、項目数がもっと増え、入力形式も多様になるかと思います。**
その時を想定すると、、`data`の管理だけでしんどそうですね。

また、その際には`computed`オブジェクトの`validation`のreturn値のキーと値の数も増えます。
そのことを考えると、この実装を実案件でつかうのはキビシそうですね。。(´・ω・｀)

# おさらい
ここまではVue.jsのこのあたりのものを使いました。

- [v-model](https://jp.vuejs.org/v2/guide/forms.html)
フォームの要素の入力値を取得する
- [v-show](https://jp.vuejs.org/v2/guide/conditional.html#v-show)
バリデーションのエラーメッセージを表示する
- [computed](https://jp.vuejs.org/v2/guide/computed.html)
バリデーションを定義する 
- [クラスバインディング](https://jp.vuejs.org/v2/guide/class-and-style.html)
登録ボタンにクラスを割り当てる

# ここからは
今から実装するコンポーネント使用ver.でも上記の仕組みを使います。
それに加え、**Vueインスタンス（親）とコンポーネント（子）の関係**が出てきます。
この**親子間でデータの通知をする**という考え方が全てのベースになってきます。

```
Vue では、親子のコンポーネントの関係は、props down, events up というように要約することができます。
親は、 プロパティを経由して、データを子に伝え、子はイベントを経由して、親にメッセージを送ります。
```
![props-events.png](https://qiita-image-store.s3.amazonaws.com/0/111637/491ec361-f184-0615-2b6d-3801b29818ec.png)


[公式より引用](https://jp.vuejs.org/v2/guide/components.html#コンポーネントの構成)

# 2. コンポーネントを使って実装する

ここまで作ったコンポーネントなしver.のファイル郡に追加して、下記のファイルを作成します。

JavaScriptファイルの構成

```
./app/assets/javascripts/registrations/
├── components
│   ├── formAppInputText.js　   # コンポーネント使用ver.のコンポーネント（郵便番号以外の項目用）
│   └── formAppInputZipCode.js　# コンポーネント使用ver.のコンポーネント（郵便番号用）
├── form.js
└── formApp.js                  # コンポーネント使用ver.のRootインスタンス
```

Viewの構成

```
./app/views/registrations/
├── _form.html.erb
├── _form_app.html.erb　# コンポーネント使用ver.のパーシャル
└── index.html.erb
```

## コンポーネントの構成図
以下の各四角の単位でコンポーネント化を行います。
コンポーネントは用途を考えて2種類作りました。
![スクリーンショット 2017-12-12 15.39.01.png](https://qiita-image-store.s3.amazonaws.com/0/111637/3756dd9f-f43c-fa87-3b72-04706bffd7ec.png)


## 登録フォーム画面を描画
HTML側

```html:app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text></form-app-input-text>
      <form-app-input-text></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text></form-app-input-text>
      <form-app-input-text></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code></input-zip-code>
      -
      <input-zip-code></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text></form-app-input-text>
    <form-app-input-text></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>
```

JavaScript側

親となるRootインスタンスを作成

```js:app/assets/javascripts/registrations/formApp.js
$(function () {
  new Vue({
    el: '#registrations-app'
  });
});
```

コンポーネントを作成

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" class="form-control"></div>' +
    '<small class="text-danger">入力してください</small>' +
    '</div>'
});
```

```js:app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" class="form-control"></div>' +
    '<small class="text-danger">正しい形式で入力してください</small>' +
    '</div>'
});
```

現時点でこのような画面が描画されています

![スクリーンショット 2017-12-07 11.10.23.png](https://qiita-image-store.s3.amazonaws.com/0/111637/66e5da0e-fbf1-858f-1960-bd6a14d95cd5.png)


## プレースホルダーを表示する

HTMLのinput属性にplaceholder属性（↓）を追加します。
![スクリーンショット_2017-12-07_11_55_55.png](https://qiita-image-store.s3.amazonaws.com/0/111637/1d3f511a-b821-cd07-e869-b316313c53da.png)


HTML側で、コンポーネントタグの`placeholderプロパティ`にデータをセット。

```html:app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :placeholder="'田中'"></form-app-input-text>
      <form-app-input-text :placeholder="'太郎'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :placeholder="'タナカ'"></form-app-input-text>
      <form-app-input-text :placeholder="'タロウ'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :placeholder="'111'"></input-zip-code>
      -
      <input-zip-code :placeholder="'2222'"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :placeholder="'東京都'"></form-app-input-text>
    <form-app-input-text :placeholder="'中央区新川1-2-3'"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :placeholder="'Xビル５F'"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>
```

コンポーネント側では、`props`オブジェクトに`placeholderプロパティ`を定義。（HTML側でコンポーネントタグに定義されれいるプロパティと同じ名称にすること）
親側で設定した`placeholderプロパティ`の文字列データが受け取れるので、そのデータをHTMLのinput要素のplaceholder属性に使用する。

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">入力してください</small>' +
    '</div>',
  props: {
    placeholder: String
  }
});
```

```js:app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    placeholder: String
  }
});
```

## 項目名と入力値を設定・取得する
各項目のバリデーションを実装していくために、まずは、コンポーネント化している項目の名称と値の設定・取得ができるようにします。
`props`の`column`プロパティを通じて、HTML側でセットした項目名をコンポーネント側で取得できるようにします。

```html:app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyName'" :placeholder="'田中'"></form-app-input-text>
      <form-app-input-text :column="'firstName'" :placeholder="'太郎'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyNameKana'" :placeholder="'タナカ'"></form-app-input-text>
      <form-app-input-text :column="'firstNameKana'" :placeholder="'タロウ'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :column="'zipCode3digit'" :placeholder="'111'"></input-zip-code>
      -
      <input-zip-code :column="'zipCode4digit'" :placeholder="'2222'"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :column="'prefecture'" :placeholder="'東京都'"></form-app-input-text>
    <form-app-input-text :column="'city'" :placeholder="'中央区新川1-2-3'"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :column="'buildingName'" :placeholder="'Xビル５F'"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>
```

また、入力値を取得できるようにinputタグに`v-model`を追加します。

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  }
});
```

こちらも同じく`v-model`を追加。（郵便番号専用のコンポーネントなので、`v-model`名はzipCodeと設定）

```js:app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      zipCode: ''
    }
  }
});
```

ここまでの実装をdevtoolsで確認すると、項目名と入力値を取得できているのが確認できました。
<br>
![スクリーンショット_2017-12-08_10_53_45.png](https://qiita-image-store.s3.amazonaws.com/0/111637/0edb1e57-d723-89bb-0b03-5930f1a0c967.png)

## 各項目のバリデーションを実装する

### バリデーション実行の概要図
各項目のバリデーションの実装部分はコンポーネントなしver.とほぼ同じですが、全項目の実装部分が違ってきます。

主に赤文字、実線矢印がコンポーネント特有の処理になります。
![スクリーンショット 2017-12-12 15.24.30.png](https://qiita-image-store.s3.amazonaws.com/0/111637/26df9fe1-d79d-949c-b606-c1be33eb1f67.png)
### 氏名・住所

まずは、バリデーションについて入力値があればOKな実装を行います。
コンポーネントを使用しないver.と同じく、`computed`を使い、入力している値が変わる度にバリデーションを実行するようにします。

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      return this.inputValue.trim();
    }
  }
});
```

これで、郵便番号以外の項目は入力があればエラーメッセージは表示されないようになりました。

### フリガナ

次にフリガナを実装します。入力値は全角カタカナのみ受け入れます。

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-if="/Kana$/.test(column)" v-show="!validationKana">カタカナで入力してください</small>' +
    '<small class="text-danger" v-else v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      return this.inputValue.trim();
    },
    validationKana: function () {
      var kanaPattern = /^[\u30a0-\u30ff]+$/;
      return kanaPattern.test(this.inputValue.trim());
    }
  }
});
```

フリガナも他項目と共通のコンポーネントを使っているので、フリガナの場合（`column`値にKanaという文字列が含まれる場合）は、フリガナ用に定義した`validationKana` 
computedを実行するように`v-if`ディレクティブを使い実装しています。

### 郵便番号

郵便番号は、半角数字3桁と4桁のみ受け入れるように実装します。

```js:app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-show="!validationZipCode">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    column: String,
    placeholder: String
  },
  data: function () {
    return {
      zipCode: ''
    }
  },
  computed: {
    validationZipCode: function () {
      var zipCode = this.zipCode.trim();
      var digit = (this.column === 'zipCode3digit') ? 3 : 4;
      var pattern = /^\d+$/;
      return (pattern.test(zipCode) && zipCode.length === digit);
    }
  }
});
```

ここまでで、各項目の**コンポーネント単位**でのバリデーションの実装はできました！

次の**全項目のバリデーション結果を見て登録ボタンの制御を行う**には、コンポーネント特有の実装が必要になります。

## 全てのバリデーションがOKの場合のみ登録ボタンが押せるようにする

### 各項目のバリデーション結果を参照する

各項目のバリデーション結果を参照するために、オブジェクト形式で返り値を返すようにしていきます。
返り値はこのような連想配列のイメージです。

```
{
  familyName: "鈴木",    # 入力値をtrim（）した値が入る
  firstName: "一郎",
  familyNameKana: true,  # testメソッド実行結果がBoolean型で入る
  familyNameKana: true
} 
```

結果格納用のオブジェクト`user`をVueインスタンス側で作成し、コンポーネント側ではデータが受け取れるように`props`のプロパティに追加します。
HTML側で、`user`プロパティにデータをセットします。今回は今までのcolumnなどとは異なり、文字列でなく**Vueインスタンスで作成したuserオブジェクト**を値として与えます。

```html:app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyName'" :user="user" :placeholder="'田中'"></form-app-input-text>
      <form-app-input-text :column="'firstName'" :user="user" :placeholder="'太郎'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyNameKana'" :user="user" :placeholder="'タナカ'"></form-app-input-text>
      <form-app-input-text :column="'firstNameKana'" :user="user" :placeholder="'タロウ'"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :column="'zipCode3digit'" :user="user" :placeholder="'111'"></input-zip-code>
      -
      <input-zip-code :column="'zipCode4digit'" :user="user" :placeholder="'2222'"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :column="'prefecture'" :user="user" :placeholder="'東京都'"></form-app-input-text>
    <form-app-input-text :column="'city'" :user="user" :placeholder="'中央区新川1-2-3'"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :column="'buildingName'" :user="user" :placeholder="'Xビル５F'"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info">Submit</button>
</div>
```

```js:app/assets/javascripts/registrations/formApp.js
$(function () {
  new Vue({
    el: '#registrations-app',
    data: {
      user: {
        type: Object
      }
    }
  });
});
```

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-if="/Kana$/.test(column)" v-show="!validationKana">カタカナで入力してください</small>' +
    '<small class="text-danger" v-else v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    user: Object,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      this.user[this.column] = this.inputValue.trim();
      return this.inputValue.trim();
    },
    validationKana: function () {
      var kanaPattern = /^[\u30a0-\u30ff]+$/;
      var result = kanaPattern.test(this.inputValue.trim());
      this.user[this.column] = result;
      return result;
    }
  }
});
```

```js:app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<div><input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-show="!validationZipCode">正しい形式で入力してください</small>' +
    '</div>',
  props: {
    column: String,
    user: Object,
    placeholder: String
  },
  data: function () {
    return {
      zipCode: ''
    }
  },
  computed: {
    validationZipCode: function () {
      var zipCode = this.zipCode.trim();
      var digit = (this.column === 'zipCode3digit') ? 3 : 4;
      var pattern = /^\d+$/;
      var result = (pattern.test(zipCode) && zipCode.length === digit);
      this.user[this.column] = result;
      return result;
    }
  }
});
```

ここまでの実装で、userオブジェクトに各項目のバリデーション結果を格納できていることが確認できます。
<br>
![スクリーンショット 2017-12-08 14.10.36.png](https://qiita-image-store.s3.amazonaws.com/0/111637/927ca262-f7d2-6c49-a602-558f2c979ebd.png)

### 結果を親へ通知し、ボタンを制御する

コンポーネント側では、`$emit`を使って親側に通知する処理を追加。引数としてuserオブジェクトを与える。

```js:app/assets/javascripts/registrations/components/formAppInputText.js
Vue.component('form-app-input-text', {
  template: '<div>' +
    '<div><input type="text" v-model="inputValue" class="form-control" :placeholder="placeholder"></div>' +
    '<small class="text-danger" v-if="/Kana$/.test(column)" v-show="!validationKana">カタカナで入力してください</small>' +
    '<small class="text-danger" v-else v-show="!validationPresence">入力してください</small>' +
    '</div>',
  props: {
    column: String,
    user: Object,
    placeholder: String
  },
  data: function () {
    return {
      inputValue: ''
    }
  },
  computed: {
    validationPresence: function () {
      this.user[this.column] = this.inputValue.trim();
      this.$emit('update-status-from-child', this.user);
      return this.inputValue.trim();
    },
    validationKana: function () {
      var kanaPattern = /^[\u30a0-\u30ff]+$/;
      var result = kanaPattern.test(this.inputValue.trim());
      this.user[this.column] = result;
      this.$emit('update-status-from-child', this.user);
      return result;
    }
  }
});

```

郵便番号のコンポーネントも同様に通知処理を追加。

```js:app/assets/javascripts/registrations/components/formAppInputZipCode.js
Vue.component('input-zip-code', {
  template: '<div>' +
    '<input type="text" v-model="zipCode" class="form-control" :placeholder="placeholder">' +
    '<span class="text-danger" v-show="!validationZipCode">半角数字・規定の桁数で入力してください</span>' +
    '</div>',
  props: {
    column: String,
    placeholder: String,
    user: Object
  },
  data: function () {
    return {
      zipCode: ''
    };
  },
  computed: {
    validationZipCode: function () {
      var zipCode = this.zipCode.trim();
      var digit = (this.column === 'zipCode3digit') ? 3 : 4;
      var pattern = /^\d+$/;
      var result = (pattern.test(zipCode) && zipCode.length === digit);
      this.user[this.column] = result;
      this.$emit('update-status-from-child', this.user);
      return result;
    }
  }
});

```

親側のVueインスタンスでは子からの通知を受けて、実行するメソッドを定義。
このメソッドでは全ての項目がクリアしているかを見て変数`status`に結果を格納しています。

```app/assets/javascripts/registrations/formApp.js
$(function () {
  new Vue({
    el: '#registrations-app',
    data: {
      user: {
        type: Object
      }
    },
    methods: {
      updateStatus: function () {
        var _this = this;
        _this.status = Object.keys(_this.user).every(function (key) {
          return _this.user[key];
        });
      }
    }
  });
});

```

HTML側は、カスタムイベント`update-status-from-child`が呼ばれたら、`updateStatus`メソッドを実行するように全てのコンポーネントタグに追加しています。

```html:app/views/registrations/_form_app.html.erb
<div id="registrations-app" class="container">
  <h3>コンポーネント利用ver.</h3>
  <div class="form-group">
    <label>氏名</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyName'" :user="user" :placeholder="'田中'" @update-status-from-child="updateStatus"></form-app-input-text>
      <form-app-input-text :column="'firstName'" :user="user" :placeholder="'太郎'" @update-status-from-child="updateStatus"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>フリガナ</label>
    <div class="form-inline">
      <form-app-input-text :column="'familyNameKana'" :user="user" :placeholder="'タナカ'" @update-status-from-child="updateStatus"></form-app-input-text>
      <form-app-input-text :column="'firstNameKana'" :user="user" :placeholder="'タロウ'" @update-status-from-child="updateStatus"></form-app-input-text>
    </div>
  </div>

  <div class="form-group">
    <label>郵便番号</label>
    <div class="form-inline">
      <input-zip-code :column="'zipCode3digit'" :user="user" :placeholder="'111'" @update-status-from-child="updateStatus"></input-zip-code>
      -
      <input-zip-code :column="'zipCode4digit'" :user="user" :placeholder="'2222'" @update-status-from-child="updateStatus"></input-zip-code>
    </div>
  </div>

  <div class="form-group">
    <label>住所</label>
    <form-app-input-text :column="'prefecture'" :user="user" :placeholder="'東京都'" @update-status-from-child="updateStatus"></form-app-input-text>
    <form-app-input-text :column="'city'" :user="user" :placeholder="'中央区新川1-2-3'" @update-status-from-child="updateStatus"></form-app-input-text>
    <span>マンション・ビル名</span>
    <form-app-input-text :column="'buildingName'" :user="user" :placeholder="'Xビル５F'" @update-status-from-child="updateStatus"></form-app-input-text>
  </div>
  <button type="button" class="btn btn-info" :disabled="!status">Submit</button>
</div>
```

これで登録ボタンの制御までできるようになりました！

# まとめ
比較しながらコンポーネントの実装をすることで、先に上げたような良いことをなんとなくでも実感できたでしょうか。

さらにこの登録フォームに、、

- この項目を削って
- 項目の並び順を変えて
- スタイルをもっとかっこよくして
- 郵便番号をいれたら住所が表示されるようにして

などの修正依頼が来たらどうでしょう。コンポーネント化しておくことで、該当の箇所が見つけやすく、ひとりで頑張らなくても分業しやすそうですね。
後で苦労する前にこれを機にコンポーネント化を進めていきましょう〜。