Vue.jsでフォームバリデーションをつくろう！ー実装編ー

# はじめに
こちらは[サポーターズColab](https://supporterzcolab.com/) で開催の[勉強会](https://supporterzcolab.com/event/1007/)の説明資料その2です。
その1は、[Vue.jsでフォームバリデーションをつくろう！ー環境構築編ー](https://qiita.com/matsumana07384/items/49e5e7edd40d73306070)です。

[Vue.jsでフォームバリデーションを作ってみよう！](https://qiita.com/matsumana07384/items/a8f940f681ad42c39a82) の内容を分割、アップデートしたものです。

----

## この記事に書いてあること
- 簡易アンケートフォームの作成
  - VueRouterを使ったリンクの作成
  - コンポーネントの実装
  - フォームの選択を解除したときのフォームバリデーション実装

## この記事で省いていること
- コードの一部処理
  - サーバーへのデータ送信
  - セキュリティ面のケア
- Vue.jsの深いお話

----

# 環境
- 端末
	- Mac OS X 10.15.1
- インストール済みのライブラリ
	- [node.js](https://nodejs.org/ja/)
		- v13.1.0
	- [npm](https://www.npmjs.com/)
		- 6.12.1
		
----

# 単一ファイルコンポーネントの記述方法について

実装に入る前に単一ファイルコンポーネントついて説明します。
単一ファイルコンポーネントは、Vueプロジェクトにおける拡張子が`.vue`のファイルのことを指します。
`<template>`、`<script>`、`<style>` のタグで構成されています。

``` HTML:.vue
<template>
<!-- HTMLの記述はこちら -->
</template>
<script>
export default {
  // JavaScriptの記述はこちら
}
</script>
<style scoped>
/* CSSの記述はこちら */
</style>
```

各タグの役割については下記の通りです。

__template__
HTMLの記述箇所。
HTMLのテンプレートエンジンのPUGを導入することも可能。

__script__
JavaScriptの記述箇所。
TypeScirptを導入することも可能。

__style__
CSSの記述箇所。
`scoped` をつけることで、styleをコンポーネント内に閉じ込めることができる。
PostCSSやLess、Sass、Scss、Stylusを導入することも可能。

単一コンポーネントのタグの順序に関しては、[公式ガイドの単一ファイルコンポーネントのトップレベルの属性の順序](https://jp.vuejs.org/v2/style-guide/index.html#%E5%8D%98%E4%B8%80%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AE%E3%83%88%E3%83%83%E3%83%97%E3%83%AC%E3%83%99%E3%83%AB%E3%81%AE%E5%B1%9E%E6%80%A7%E3%81%AE%E9%A0%86%E5%BA%8F-%E6%8E%A8%E5%A5%A8)を参照ください。

----

# ページを追加

完了ページを作っていきます。
こちらの項目は、Vue.jsよりも`vue-router`の話が中心です。

## ファイルを追加

`src/views/`に`Done.vue`を作成し、下記を記述します。

``` HTML:src/views/Done.vue
<template>
  <div class="sucess">
    <p>完了しました！</p>
  </div>
</template>

<script>
export default {
}
</script>

<style scoped>
</style>
```

## ボタンリンクの追加

Aboutページにボタンリンクを追加し、リンクが遷移できるように設定します。
`src/views/`の`About.vue`に下記を記述します。

```HTML:src/views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <!-- ここから、追加 -->
       <router-link to="done">
          <button type="button" name="done" value="done">done</button>
       </router-link>
       <!-- ここまで、追加 -->
  </div>
</template>
```

__templete__
ボタンリンクを追加しました。
リンクに用いている`<router-link>`は、ルーターを使用しているアプリケーションにリンクを追加できるタグです。
デフォルトで、`<a>`タグと`href`で描画されます。

今回の下記の記述箇所は、

```HTML
<router-link to="done">
    <button type="button" name="done" value="done">done</button>
</router-link>
```

下記のようにブラウザに描画されます。

``` HTML
<a href="/done" class="" data-lb-orig-href="http://localhost:8081/done">
  <button type="button" name="done" value="done">done</button>
</a>
```

詳細は、[API リファレンス | Vue Router](https://router.vuejs.org/ja/api/)を参照ください。

## 追加したページを設定

現時点で先程作成したボタンリンクをクリックしても、真っ白なページが表示されます。
これは本来のSPAでは、パスを存在しないためです。

![screencapture-localhost-8080-done-2019-12-02-23_57_45](https://user-images.githubusercontent.com/35416089/70050879-52595f80-1613-11ea-8746-bccfd4949d21.png)

そのため、前述で作成した`src/views/Done.vue`を`router/index.js`に設定します。

``` Javascript：router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import { doesNotReject } from 'assert'


Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: function () {
      return import(/* webpackChunkName: "about" */ '../views/About.vue')
    }
  },//カンマを追加
  //------ ここから追加
  {
    path: '/done',
    name: 'done',
    component: function () {
      return import('../views/Done.vue')
    }
  },
  //----- ここまで追加
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

```

- path:作成したページのpathを設定
- name:ルートを特定するための名前を設定（なくてもOK）
- component:表示するファイルを記述

ここまで設定が完了すると、`/about`にあるボタンリンクをクリックで画面が遷移し、設定した文字が表示されます。

![be3cc373579717c6c2690d55a7154e73](https://user-images.githubusercontent.com/35416089/70157718-68415000-16f9-11ea-84b9-f984b608fa48.gif)

----

ここからアンケートをつくる準備を進めていきます。

# コンポーネントを作成

`src/components/` の配下に `Questionnaire.vue` をファイルを作成し、下記を記述します。

``` HTML:src/components/Questionnaire.vue
<template>
<div>
  <p><b>{{title}}</b></p>
</div>
</template>

<script>
export default {
  props: {
      title : String
  }
}
</script>

<style scoped>
</style>
```

__template__
`script`で宣言した `title` を表示できるよう記述

__script__
`src/views/About.vue` のテンプレートで `src/components/Questionnaire.vue` を使用するため、`About.vue`が親、`Questionnaire.vue` が子という関係になります。

Vue.jsは[単方向のデータフロー](https://jp.vuejs.org/v2/guide/components-props.html#%E5%8D%98%E6%96%B9%E5%90%91%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%83%95%E3%83%AD%E3%83%BC)のため、親と子の間でデータをやり取りする際には宣言が必要です。

親から子へデータを渡す際には`props`を用い、 子から親へデータを渡す際は`カスタムイベント`を用います。

- `props`の詳細は、[公式ガイドのprops](https://jp.vuejs.org/v2/api/#props)を参照
- `カスタムイベント`の詳細は、[公式ガイドのカスタムイベント](https://jp.vuejs.org/v2/guide/components-custom-events.html#ad)を参照

----

## About.vueからコンポーネントを使用

`src/views/About.vue` から `src/components/Questionnaire.vue` を呼び出します。

``` HTML:src/views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <!-- ここから追加 -->
    <Questionnaire title="アンケート"></Questionnaire>
    <!-- ここまで追加 -->
    <router-link to="done">
      <button type="button" name="done" value="done">done</button>
    </router-link>
  </div>
</template>

<script>
// 上から追加
import Questionnaire from '@/components/Questionnaire.vue'// 追加

export default {
  name: 'about',
  components: {
    Questionnaire
  }
}
//下まで追加
</script>
```

__template__
`script`で登録したコンポーネントを `Questionnaire` タグとして使用します。
`title` に`アンケート`という文字列を親から子に渡しています。

__script__
`components`のオプション内に`Questionnaire`をコンポーネントとして定義しています。

----

# 入力フォームバリデーションを作成

ここから`src/components/Questionnaire.vue` にフォームを作り、入力した値をバインディングする処理を追加します。

## フォームの作成とデータバインディングを設定

``` HTML:src/components/Questionnaire.vue
<template>
<div>
  <p><b>{{ title }}</b></p>
<!-- ここから追加 -->
  <form>
      <p>Hello, {{ questionnaire.nickName }}</p>
    <div>
      <label>あだ名：</label>
      <input type="text" name="nickname" placeholder="呼ばれたい名前をどうぞ" v-model="questionnaire.nickName">
    </div>
      <p>
        <label>TwitterID：</label>
        <input type="text" name="belong" value="" placeholder="ないかたはスキップOK">
      </p>
      <p>
        <label>今日の勉強会との関わり：</label>
        <input type="text" name="connection" value="" placeholder="思いの丈をどうぞ！">
      </p>
      <button type="submit">submit</button>
  </form>
<!-- ここまで追加 -->
</div>
</template>

<script>
export default {
  props: {
      title : String
  },//カンマを追加
  //------ ここから追加
  data: function() {
    return {
      questionnaire: {
        nickName: null,
      }
    }
  }
  //------ ここまで追加
}
</script>

<style scoped>
</style>
```

__templete__
データとフォームの入力項目をバインドするには、 `v-model` を使用します。
`v-model`の詳細は、[公式ガイドのv-model](https://jp.vuejs.org/v2/api/#v-model)を参照ください。

__script__
`data` は、アプリケーションで使用するデータを記述します。
`data`の詳細は、[公式ガイドのdata](https://jp.vuejs.org/v2/api/#data)を参照ください。

現時点ではこのような状態かと思います。
![screencapture-localhost-8080-about-2019-12-04-23_11_42](https://user-images.githubusercontent.com/35416089/70151143-83f32900-16ee-11ea-8ad4-f3f55871e25d.png)

----

## ボタンの削除

前述で`src/views/About.vue`にて作成したボタンリンクですが、使用しないため下記箇所を削除します。

``` HTML:src/views/About.vue
<router-link to="done">
  <button type="button" name="done" value="done">done</button>
</router-link>
```

----

## フォームのバリデーションを設定

フォームから選択が外れた際にバリデーションチェックを行う処理を実装します。
今回はTwitterIDのフォームに追加します。

TwitterIDの条件は、[ユーザー名の登録のヘルプ](https://help.twitter.com/ja/managing-your-account/twitter-username-rules)を参照すると…

>ユーザー名の長さは15文字までです。名前は50文字までですが、ユーザー名は使いやすいように短くなっています。

>ユーザー名には英数字（文字A～Z、数字0～9）しか含めることはできません。ただし、上記のとおりアンダースコア（_）は例外です。希望するユーザー名に、記号やダッシュ、スペースが含まれていないことを確認してください。

上記の通りのため、下記の2つをエラーを表示する条件として追加します。

- 半角英数以外で入力されている場合
- 15文字以上の場合

``` HTML:src/components/Questionnaire.vue
<template>
<div>
  <p><b>{{ title }}</b></p>
<!-- ここから追加 -->
  <form v-on:submit.prevent="onSubmit">
    <div>
      <p>Hello, {{ questionnaire.nickName }}</p>
      <label>あだ名：</label>
      <input type="text" name="nickname" placeholder="呼ばれたい名前をどうぞ" v-model="questionnaire.nickName">
    </div>
    <div>
      <label>TwitterID：</label>
      <input type="text" name="belong" value="" placeholder="ないかたはスキップOK" v-model="questionnaire.twitterID" v-on:change="checkForm">
    </div>
    <div>
      <label>今日の勉強会との関わり：</label>
      <input type="text" name="connection" value="" placeholder="思いの丈をどうぞ！">
    </div>
    <p class="error"> {{ validation.result }}</p>
    <button v-on:click="checkForm">submit</button>
  </form>
<!-- ここまで追加 -->
</div>
</template>

<script>
export default {
  props: {
      title : String
  },
  data: function() {
    return {
      questionnaire: {
        nickName: null,
        twitterID: null,//追加
      },//カンマを追加
      //------ ここから追加
      validation:{
        result: "",
      }
      //------ ここまで追加
    }
  },/*ここにカンマを追加*/
  //------ ここから追加
  methods: {
    checkForm: function(event){
      var booleanTwitterID = false
      var inputTwitterID = this.questionnaire.twitterID

      if(!this.checkString(inputTwitterID)){
        this.validation.result = "半角英数字および_のみで入力ください"
      }
      else if(!this.checkMaxLength(inputTwitterID)){
        this.validation.result = "15文字以内で入力ください"        
      }
      else {
        booleanTwitterID = true
      }

      if(booleanTwitterID === true){
        this.validation.result=""
        alert('Hello,' + inputTwitterID + '!')
      } 
      event.preventDefault()
    },
    checkString: function(inputdata){
      var regExp = /^[a-zA-Z0-9_]*$/
      return regExp.test(inputdata);
    },
    checkMaxLength: function(inputdata){
      var booleanLength = false
      inputdata.length <= 15 ? booleanLength = true : booleanLength = false;
      return booleanLength
    }
  }
  //------ ここまで追加
}
</script>

<style scoped>
/* ここから追加 */
.error { color: red; }
/* ここまでを追加 */
</style>
```

__template__
`v-on:submit.prevent="onSubmit"` は、submit イベントによってページがリロードされないイベント修飾子です。

`v-on:change`は、カスタムイベントの一種で変更を取得することができます。

__script__
`<script>` 内の `methods` は、アプリケーションで使用するメソッドを指定します。
処理の分割やイベントハンドラなどを記述します。

----

ここまでの記述を該当ファイルに追加すれば、完成です🎉
TwitterIDのフォームに半角英数字以外や15文字以上を入力した状態でフォームの選択を外して、下記の挙動が再現されます。

![3b64e42643e2116ceaa97106946ca0af.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186755/38c90e00-77f3-c3a4-3fe7-07ae45c3b268.gif)

----

このアンケートフォームにはいろいろな機能が足りていません。
ここからカスタマイズをして、自分なりの最高のUIのアンケートを実装してみてください！

----

# その他のオプション
今回は使用していませんが、Vue.jsを理解する上で重要なオプションをご紹介します。

__computed__
任意のデータを処理を返す算出プロパティ。

- 詳細は、[算出プロパティとウォッチャ — Vue.js 内の算出プロパティ](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)を参照ください。

__watch__
特定のデータや算出プロパティの状態を監視、データの変化で処理を実行するプロパティ。

- 詳細は[算出プロパティとウォッチャ — Vue.js 内のウォッチャ](https://jp.vuejs.org/v2/guide/computed.html#%E3%82%A6%E3%82%A9%E3%83%83%E3%83%81%E3%83%A3)を参照ください。

----

# さいごに
ここまで頑張ってフォームのバリデーションを実装しましたが、世の中にはかんたんにバリデーションを設定できる素晴らしいライブラリが提供されています。

- [vuelidate](https://github.com/vuelidate/vuelidate)
- [VeeValidate](https://github.com/logaretm/vee-validate)

これらを使えば、よりよいUXのフォームバリデーションをスムーズに開発ができます。
本当にいい時代に生まれました。

----

他にも、Vue.jsにはたくさんの[サポートライブラリ](https://github.com/vuejs/awesome-vue#components--libraries)があり、いろんなことが実現できる環境が整っています。

今回のハンズオンでは、Vue.jsの中でも一部機能にしか触れておりません。
もっと知りたい！と思って頂けた方は、ぜひ[公式ドキュメント](https://jp.vuejs.org/v2/guide/)を読んだり、コミュニティに参加したりしてみてください！

ここまでご参加（お読み）頂き、ありがとうございました🙏🏻