【Vueの実践練習に最適】入力フォームのエラーをリアクティブにチェックしたい

# 記事の目的
* Vue.jsを使って、入力フォームのエラーをリアクティブにチェックする。
* Vue.jsを学び始めた初心者に実践的な例を提供する。


# ゴールのイメージ
ページ全体のイメージはこちら。アカウント作成画面を作ります。
ページ中のパスワード入力フォームのエラーチェックが今回のメインテーマです。
![スクリーンショット 2025-02-22 15.00.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281138/7d036169-260d-4773-b628-fc0c136e5440.png)

実践的な例になるようにUIも工夫しています。

エラーのリアクティブな検知のイメージはこちら。
![エラー検知.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281138/7286b782-a9ce-414b-ae43-16ea213338b9.png)



# 要件
パスワードの入力フォームのヴァリデーション
* 文字数が８文字以上
* 半角英数記号のみ

UIの要件
* パスワードの表示・非表示を切り替えられる。
* 入力中の文字数がわかる。



# コード

* 入力フォームに`@input="validatePassword"`を設置することで、入力すると`validatePassword()`メソッドが機能します。
* `v-model="password"`を設置することで、入力中のテキストがリアクティブに`password`プロパティに反映されます。その内容を`validatePassword()`メソッドがジャッジすることで、リアクティブにエラーを検知できます。
* エラーが発生しているときのみ、`errors`プロパティにエラー文言を挿入します。
* `v-if`により、`errors`プロパティが空ではないときのみエラーテキストを画面に表示します。
* `v-for`を使い複数のエラー文言すべてを出力します。

## html
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Password Input</title>
    <!-- Vue.js CDN -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <!-- Google Material Icons -->
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet" />
    <link
      href="index.css"
      rel="stylesheet" />

  </head>

  <body>
    <div id="app">
      <div class="mobile-size">
        <h1>ServiceName</h1>
        <h2>アカウント作成</h2>
        <p>アカウントの作成が必要です。必要な情報を入力してください。</p>
        <p>アカウントの作成が必要です。必要な情報を入力してください。</p>
        <h3>氏名<span class="ness">必須</span></h3>
        <input class="custom-input" />
        <br />
        <h3>メールアドレス<span class="ness">必須</span></h3>
        <input class="custom-input" />
        <br />

        <h3>パスワード<span class="ness">必須</span></h3>
        <p>8文字以上にしてください。</p>
        <p>半角英数記号のみ使用できます。</p>
        <div class="form-group">
          <input
            id="password"
            class="custom-input"
            :type="showPassword ? 'text' : 'password'"
            v-model="password"
            @input="validatePassword" />
          <span class="material-icons" @click="togglePasswordVisibility">
            {{ showPassword ? 'visibility_off' : 'visibility' }}
          </span>
        </div>
        <p class="character-count">文字数: {{ password.length }} / 8</p>
        <ul class="error-list" v-if="errors.length > 0">
          <li v-for="error in errors" :key="error">{{ error }}</li>
        </ul>
        <br />
        <button class="button-modern">送信する</button>
      </div>
    </div>

    <script src="index.js"></script>
  </body>
</html>

```

## js
初心者向けに、くどいくらいにコメントを挿入しています。
```javascript
// Vue アプリケーションを作成する
const app = Vue.createApp({
  // data() 関数: アプリケーションの状態（データ）を定義する
  data() {
    return {
      password: "",       // 入力されたパスワードを保存する変数（初期値は空文字）
      showPassword: false, // パスワードの表示状態（falseなら非表示、trueなら表示）
      errors: [],         // エラーメッセージを格納する配列（初期値は空）
    };
  },

  // methods: 画面上で実行する関数（ボタンを押したときの動作など）を定義する
  methods: {
    // パスワードの表示・非表示を切り替える関数
    togglePasswordVisibility() {
      this.showPassword = !this.showPassword;
      // `!this.showPassword` は `true` を `false` に、`false` を `true` に切り替える
    },

    // パスワードの入力チェックを行う関数
    validatePassword() {
      this.errors = []; // エラーメッセージをリセット（毎回新しくチェックするため）

      // パスワードの長さが8文字未満ならエラーを追加
      if (this.password.length < 8) {
        this.errors.push("8文字以上にしてください。");
      }

      // 正規表現を使い、許可されていない文字が含まれているかチェック
      // `/[^a-zA-Z0-9!@#$%^&*()]/` は、英数字と特定の記号以外を見つける
      // `.test(this.password)` はパスワードにその文字が含まれているか調べる
      if (/[^a-zA-Z0-9!@#$%^&*()]/.test(this.password)) {
        this.errors.push("使用できる文字は半角英数字と記号のみです。");
      }
    },
  },
});

// Vue アプリケーションを `#app` 要素に適用（Vue を起動）
app.mount("#app");
```

## CSS
```css
body {
  font-family: Arial, sans-serif;
  padding: 20px;
}

.mobile-size {
  width: 393px;
  height: 852px;
  background-color: #dddddd;
  margin: 10px;
  padding: 18px;
}

h1 {
  border-width: 6px;
  border-bottom: double;
  padding-bottom: 6px;
  color: #373737;
}

h2 {
  color: #373737;
}

h3 {
  color: #373737;
  margin-top: 24px;
  margin-bottom: 4px;
}

p {
  color: #373737;
  margin-top: 4px;
  margin-bottom: 4px;
  line-height: 1.6;
}

.ness {
  font-size: 70%;
  margin-left: 4px;
  padding: 2px;
  background-color: #b3d0d7;
}


.form-group {
  position: relative;
  /* アイコンを右端に配置するためにrelative */
  width: 100%;
  margin-bottom: 10px;
}

.custom-input {
  width: 100%;
  height: 40px;
  padding: 10px;
  padding-right: 40px;
  font-size: 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

.material-icons {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #666;
}

.material-icons:hover {
  color: #333;
}

.character-count {
  margin-top: 5px;
  font-size: 0.9em;
  color: #333;
}

.error-list {
  color: red;
  font-size: 0.9em;
}

.error-list li {
  margin-bottom: 5px;
}

.button-modern {
  background-color: #82aaaa; /* モダンな青色 */
  color: #373737; /* 白文字 */
  font-size: 16px; /* 適度なフォントサイズ */
  font-weight: 600; /* 少し太めの文字 */
  padding: 12px 20px; /* 内側の余白 */
  border: none; /* 枠線を非表示 */
  border-radius: 8px; /* 角丸 */
  cursor: pointer; /* ポインターカーソル */
  transition: all 0.3s ease; /* ホバー時のアニメーション */
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* さりげない影 */
  margin-top: 16px;
}

```