5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue #2Advent Calendar 2019

Day 23

Vue.jsで入力フォームにバリデーションをつける

Last updated at Posted at 2019-12-22

はじめに

この記事はVue#2アドベントカレンダーの23日目です🎅

アドベントカレンダー初参加の@yakumomutsukiです。
当初は「vue-cliを使わなくてもできるvueを使った開発」を考えていたのですが、書いているうちにただのWebpackの説明になってしまったので、実務であるあるなお悩みを書いてみようと思いました。

バリデーション + 入力フォーム

フロントエンドの開発において、フォーム周りのバリデーションの設計は比較的悩みどころかなあと思います。zipというプロパティにたいして、watchを使ってバリデーションを張るか、computedを使うか、このあたりは実装者によってさまざまあると思います。

さまざまあるなかでの「私はこう実装した」という一例をご覧ください。

App.vue
<template>
  <div id="app">
    <div class="container">
      <form>
        <div class="form-row">
          <div class="form-group col-md-6">
            <label for="email">メールアドレス(必須)</label>
            <input
              id="email"
              required
              type="email"
              class="form-control"
              :class="formError.email.className"
              name="email"
              placeholder="メールアドレスを入力してください"
              @change="e => update(e)"
              :value="formState.email"
            />
            <span>{{ formError.email.errorMessage }}</span>
          </div>
          <div class="form-group col-md-6">
            <label for="password">パスワード(必須)</label>
            <input
              id="password"
              required
              type="password"
              class="form-control"
              :class="formError.password.className"
              name="password"
              placeholder="パスワード"
              @change="e => update(e)"
              :value="formState.password"
            />
            <span>{{ formError.password.errorMessage }}</span>
          </div>
        </div>
        <div class="form-row">
          <div class="form-group col-md-4">
            <label for="zip">郵便番号(必須)</label>
            <input
              id="zip"
              required
              type="text"
              class="form-control"
              :class="formError.zip.className"
              name="zip"
              placeholder="1000000"
              maxlen="7"
              @change="e => update(e)"
              :value="formState.zip"
            />
            <p>{{ formError.zip.errorMessage }}</p>
          </div>
          <div class="form-group col-md-4">
            <label for="state">都道府県(必須)</label>
            <select
              id="state"
              class="form-control"
              :class="formError.state.className"
              :value="formState.state"
              @change="e => update(e)"
              required
            >
              <option value="" selected>選択してください</option>
              <option value="tokyo">東京</option>
              <option value="osaka">大阪</option>
            </select>
            <p>{{ formError.state.errorMessage }}</p>
          </div>
          <div class="form-group col-md-4">
            <label for="city">市区町村(必須)</label>
            <input
              id="city"
              required
              type="text"
              class="form-control"
              :class="formError.city.className"
              name="city"
              placeholder="千代田区"
              @change="e => update(e)"
              :value="formState.city"
            />
            <p>{{ formError.city.errorMessage }}</p>
          </div>
        </div>
        <div class="form-group">
          <label for="address1">住所1(必須)</label>
          <input
            id="address1"
            required
            type="text"
            class="form-control"
            :class="formError.address1.className"
            name="address1"
            placeholder="秋葉原1-1-1"
            @change="e => update(e)"
            :value="formState.address1"
          />
          <p>{{ formError.address1.errorMessage }}</p>
        </div>
        <div class="form-group">
          <label for="address2">住所2</label>
          <input
            id="address2"
            type="text"
            class="form-control"
            name="address2"
            placeholder="秋葉原ビル1F"
            @change="e => update(e)"
            :value="formState.address2"
          />
        </div>
        <button class="btn btn-primary" @click="registration">登録する</button>
      </form>
    </div>
  </div>
</template>

<script>
const errorBase = {
  errorMessage: "",
  className: ""
};
const patterns = {
  password: /^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]{8,16}$/,
  zip: /^[0-9]{7}$/,
  phoneNumber: /^[0-9]{10,11}$/
};

import isEmail from "validator/lib/isEmail";
import isMatch from "validator/lib/matches";

export default {
  name: "App",
  data() {
    return {
      formState: {
        email: "",
        password: "",
        zip: "",
        state: "",
        city: "",
        address1: "",
        address2: ""
      },
      formError: {
        email: { ...errorBase },
        password: { ...errorBase },
        zip: { ...errorBase },
        state: { ...errorBase },
        city: { ...errorBase },
        address1: { ...errorBase },
        address2: { ...errorBase }
      }
    };
  },
  methods: {
    /**
     * 値が更新されたらupdateメソッドが呼ばれる
     * formStateの値を更新したら、バリデーションを実行
     * @param e HTMLElement
     */
    update(e) {
      const { name, value } = e.target;
      this.formState[name] = value;
      this.formError[name] = { ...errorBase };

      this.validate(name);
    },

    /**
     * バリデーションを行います
     * 未入力チェックが必要な場合は、第2引数にfalseを指定する
     * @param name
     * @param allowBlank
     */
    validate(name, allowBlank = true) {
      const formVal = this.formState[name];

      // 未入力チェックをする場合
      if (!allowBlank && !formVal) {
        this.formError[name] = {
          errorMessage: "未入力です",
          className: "error"
        };
        return;
      }

      // メールアドレス
      if (name === "email" && formVal && !isEmail(formVal)) {
        this.formError[name] = {
          errorMessage: "メールアドレスに誤りがあります",
          className: "error"
        };
        return;
      }

      // パスワード
      if (
        name === "password" &&
        formVal &&
        !isMatch(formVal, patterns.password)
      ) {
        this.formError[name] = {
          errorMessage: "パスワードに誤りがあります",
          className: "error"
        };
        return;
      }

      // 郵便番号
      if (name === "zip" && formVal && !isMatch(formVal, patterns.zip)) {
        this.formError[name] = {
          errorMessage: "郵便番号に誤りがあります",
          className: "error"
        };
      }
    },

    /**
     * 登録処理を行います
     * バリデーションで引っかかった場合は、登録処理を行えません
     */
    registration() {
      for (let [key] of Object.entries(this.formState)) {
        // バリデーションを行わないのはcontinueで抜けておく
        if (key === "address2") {
          continue;
        }
        // バリデーションを実行
        this.validate(key, false);
      }

      let isValid = true;
      for (let [key] of Object.entries(this.formError)) {
        // エラーメッセージがない場合にはtrueになる
        isValid = !this.formError[key].errorMessage ? isValid : false;
      }

      if (isValid) {
        window.alert("登録に成功しました");
      } else {
        window.alert("入力不備があります");
      }
    }
  }
};
</script>

<style>
.error {
  background-color: #ffecec !important;
}
</style>

お気づきになりましたでしょうか。
このコードはwatchもcomputedも使っておりません。しかし、formStateとfromErrorの各プロパティのキー名を同じにしておくことで、updateメソッドからvalidateメソッドに同じキー名を渡して、バリデーションをかけることができています。

    update(e) {
      const { name, value } = e.target;
      this.formState[name] = value;
      this.formError[name] = { ...errorBase };

      this.validate(name);
    },

引数として受け取ったHTMLElementから、const { name, value } = e.target;のように分割代入して、nameとvalueを受け取り、formStateに値を設定、fromErrorの初期化して、もう一度バリデーションという流れになっています。

おわりに

書いていて思ったのですが、別にVueじゃなくてJavaScriptのテクニック感集ですね...。v-modelやwatch、computedは使わなくてもバリデーションはできますというお話でした。
明日のアドベントカレンダーは@kacky917さんです!!🎄

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?