【Rails】JavaScriptで動的なバリデーション(リアルタイムでチェックする機能)を実装してみた

Railsでフォームのバリデーションチェックを行う場合、送信ボタンを押した後でないとエラーがあるかどうかを判定できません。

バックエンド側でバリデーションチェックは行いつつ、フロント側でもリアルタイムにバリデーションチェックを行う機構を実装したい。

そこで、今回はJavaScriptでリアルタイムにバリデーションチェックを行う機構をほぼ自力で実装してみたので、忘れないうちにメモとして残しておこうと思います。

(JavaScript学習して間もない初心者の実装なので、コードが多少冗長で非効率かもしれません。ただ、一つ一つのコードの意味や動きをしっかり追えるように書いたつもりなので、JavaScript初学者の方の参考にはなるかなと思います)

【追記】
JS + Stimulus で動的バリデーションチェックを実装してみたので記事にしました。コード量も3分の2くらいまで減らすことができ、コードの管理もしやすいので、Rails7以降で開発するなら個人的にStimulusかなりおすすめです。
【Rails7】JavaScript + Stimulusで動的なバリデーションチェックを実装してみた

目次

開発環境

  • Ruby 3.1.2
  • Ruby on Rails 7.0.3
  • Bootstrap 5.1.3
  • M1 Macbook Air 2020
  • mac OS Monterey (ver. 12.4)
  • ターミナル bash (Rosetta 2 使用

出来上がりイメージ

今回JavaScriptで実装するバリデーション機構の出来上がりイメージは以下の通りです。

  • デフォルトでフォーム送信ボタン(「登録する」ボタン)は無効化されている
  • バリデーションエラー時に、各フィールドにエラーメッセージを表示し、フィールドに赤枠線を付ける
  • バリデーションチェックに通ったらエラーメッセージを削除し、フィールドに緑枠線を付ける
  • 全てのバリデーションチェックに通ったら、フォーム送信ボタンを有効化する

以下、バリデーションチェックの動作イメージです。(バリデーションチェックを適用しているのは必須項目のフィールドのみ)

全てのチェック項目をパスしたら「登録する」ボタンが有効化されるようになっています。

メールアドレスやパスワードについては、正規表現パターンに一致した値が入力されたらパスするようにしています。

また、全てのチェック項目をパスしたとしても、フォームの入力内容変更などでバリデーションエラーが(1つでも)発生した場合は、「登録ボタン」が再び無効化されるようにしました。

前提条件

以下の前提条件で実装を進めていきます。

【前提】

  • Userモデル作成済み
  • 新規登録フォーム(view)作成済み
  • Turbo Drive(もしくはTurbolinks)有効化済み
  • Bootstrap5 導入済み

今回はJavaScriptでの動的バリデーション実装がメインなので、新規登録フォームなどのviewファイル作成部分の説明は省略します。

JavaScriptで動的バリデーション(リアルタイムチェック)機構を実装する手順

大まかな実装の流れは以下の通りです。

  1. 作成したテンプレートの入力フォームタグをquerySelector()で取得する
  2. addEventListener()で取得したタグのイベントを追加し、フォームへの入力(変更)がある度にイベントが発火するようにする
  3. フォームに入力した値を取得し、if文でバリデーション処理の内容を記述
  4. バリデーションチェックをパスしたかどうかを判断する変数を追加(activeNameなどと命名。各フィールドの数だけ用意する)
  5. バリデーションチェックをパスしたら、上記変数にtrueを代入する(エラーの場合はfalseを代入する)
  6. ④で追加した変数が全てtrueの場合はフォーム送信ボタンを有効化、そうでない場合はフォーム送信ボタンを無効化にする条件式を追加(デフォルトで無効化にしておく)

それでは詳しくみていきましょう。

作成したテンプレート(viewファイル)の入力フォームタグをquerySelector() で取得する

まずは、作成したviewファイルのフォームタグをquerySelector()で取得します。

var userForm = document.querySelector("#userForm");

querySelector("セレクタ")で、指定したセレクタの要素を取得することができます。

例えば、id="userForm" のフォームタグ<form></form>の要素全体を取得したい場合、上記のようにquerySelector("#userForm")とすることで取得できます。

取得した フォームタグ<form></form>はグローバル変数userFormとして代入しておきます。

試しにコンソールで取得したフォームタグuserFormを入力してみると、取得したフォームタグが返されることがわかります。

今回作成したviewファイルのコードです。

<div class="row justify-content-center">
  <%= form_with(model: @user, local: true, class: "mt-4 form-column-width", id: "userForm") do |f| %>

    <!----- ユーザー名(:name) ------>
    <div class="mt-3 mb-3", id="userName">
      <%= f.label :name, class: "form-label fw-bold"%>
      <span class="text-danger">*</span>
      <div class="input-group shadow-sm rounded">
        <span class="input-group-text" id="basic-addon1">
          <i class="bi bi-person-circle"></i>
        </span>
        <%= f.text_field :name, placeholder: "ユーザー名を入力してください", class: "form-control"%>
      </div>
    </div>

    <!----- 性別 ------>
   ・・・

    <!----- メールアドレス(:email) ------>
    <div class="mb-3" id="userEmail">
      <%= f.label :email, class: "form-label fw-bold"%>
      <span class="text-danger">*</span>
      <div class="input-group shadow-sm rounded">
        <span class="input-group-text" id="basic-addon1">
          <i class="bi bi-envelope"></i>
        </span>
        <%= f.email_field :email, class: "form-control", placeholder: "Eメールアドレスを入力してください" %>
      </div>
      <div id="emailHelp" class="form-text text-primary">※アカウント認証確認メールを送信する為、必ず実際に使えるメールアドレスをご使用ください</div>
    </div>

    <!----- パスワードフォームのグループ(:password, :password_confirmation) ------>
    <div id="userPasswordGroup">
      <!----- パスワード ------>
      <div class="mb-3" id="userPassword">
        <%= f.label :password, class: "form-label fw-bold"%>
        <span class="text-danger">*</span>
        <div class="input-group shadow-sm rounded">
          <span class="input-group-text" id="basic-addon1">
            <i class="bi bi-key-fill"></i>
          </span>
          <%= f.password_field :password, class: "form-control", placeholder: "パスワードを入力してください" %>
        </div>
        <div id="emailHelp" class="form-text text-primary">※6文字以上の半角英数字を入力してください</div>
      </div>

      <!----- パスワード(確認用) ------>
      <div class="mb-3" id="userPasswordConfirmation">
        <%= f.label :password_confirmation, class: "form-label fw-bold"%>
        <span class="text-danger">*</span>
        <div class="input-group shadow-sm rounded">
          <span class="input-group-text" id="basic-addon1">
            <i class="bi bi-key-fill"></i>
          </span>
          <%= f.password_field :password_confirmation, class: "form-control", placeholder: "パスワードをもう一度入力してください" %>
        </div>
      </div>
    <% end %>
  </div>

  <!----- 送信(更新)ボタン ------>
  <%= f.submit class: "btn btn-primary mb-4", id: "userSubmit" %>

  <% end %>
</div>

<!----- バリデーション用の jsコード読み込み(_form_validation.html.erb) ------>
<%= render "form_validation" %>

上記のviewファイルのコードをもとに、今回の実装であらかじめ取得しておく要素はこちらです。(青文字は<input>タグの要素)

各要素をdocument.querySelector()で取得し、各グローバル変数に代入しておきます。

<script>
  // フォーム全体の要素
  var userForm = document.querySelector("#userForm");

  // 各入力フィールド(name, email, password, password_confirmation) の要素
  var userNameField = document.querySelector("#userName");
  var userEmailField = document.querySelector("#userEmail");
  var userPasswordField = document.querySelector("#userPassword");
  var userPasswordConfirmationField = document.querySelector("#userPasswordConfirmation");

  // パスワードおよびパスワード確認用フィールドの要素グループ
  var userPasswordGroup = document.querySelector('#userPasswordGroup');

 // 各入力フィールド内の<input>タグ 
  var userName = document.querySelector("#user_name");
  var userEmail = document.querySelector("#user_email");
  var userPassword = document.querySelector("#user_password");
  var userPasswordConfirmation = document.querySelector("#user_password_confirmation");

  // フォーム送信ボタン(Submit)の要素
  var userSubmitBtn = document.querySelector("#userSubmit");
  
  // 各フィールドのエラーメッセージ表示用の paragraph
  var namePara = document.createElement('p');
  var emailPara = document.createElement('p');
  var passwordPara = document.createElement('p');
  var confirmationPara = document.createElement('p');
</script>

addEventListener() で取得した要素のイベントを追加

続いては、取得した要素に対するイベントを追加します。

例えば、取得した入力フォームの要素で、フォームに値が入力(もしくは削除)された時に何かしらのイベントを発火させたい場合は以下のように記述します。

// userNameのフォームに値を入力(もしくは削除)した時に発火するイベント
userName.addEventListener('input', ()=>{
  // イベント発火時の処理
});

今回はuserName, userEmail, userPasswordGroupに対してバリデーションを行うイベントを追加したいので、以下のように追記します。

<script>
  // フォーム全体の要素
  var userForm = document.querySelector("#userForm");

  // 各入力フィールド(name, email, password, password_confirmation) の要素
  var userNameField = document.querySelector("#userName");
  var userEmailField = document.querySelector("#userEmail");
  var userPasswordField = document.querySelector("#userPassword");
  var userPasswordConfirmationField = document.querySelector("#userPasswordConfirmation");

  // パスワードおよびパスワード確認用フィールドの要素グループ
  var userPasswordGroup = document.querySelector('#userPasswordGroup');

 // 各入力フィールド内の<input>タグ 
  var userName = document.querySelector("#user_name");
  var userEmail = document.querySelector("#user_email");
  var userPassword = document.querySelector("#user_password");
  var userPasswordConfirmation = document.querySelector("#user_password_confirmation");

  // フォーム送信ボタン(Submit)の要素
  var userSubmitBtn = document.querySelector("#userSubmit");
  
  // 各フィールドのエラーメッセージ表示用の paragraph
  var namePara = document.createElement('p');
  var emailPara = document.createElement('p');
  var passwordPara = document.createElement('p');
  var confirmationPara = document.createElement('p');

  /**************************** ↓追記↓ *******************************/
  //表示名のバリデーション
  userName.addEventListener("input", ()=>{
    // イベント発火時の処理
  });

  //メールアドレスのバリデーション
  userEmail.addEventListener("input", ()=>{
    // イベント発火時の処理
  });

  //パスワードのバリデーション
  userPasswordGroup.addEventListener("input", ()=>{
    // イベント発火時の処理
  });
/********************************************************************/

</script>

イベントの内容はこれから追加していきます。

無効化したフォーム送信ボタンの有効化方法

バリデーションのイベント内容を追加する前に、フォームの送信ボタンの有効化・無効化を切り替えるイベントも追加しておきます。

以下のように、デフォルトでは送信ボタンを無効にし、フォーム全てのバリデーションチェックをパスした場合にのみ送信ボタンを有効化するようなイベントを追加します。

上記のようなイベントを実現するためにはどのようにすれば良いでしょうか。

方法はいくつかあるかと思いますが、今回は以下のような手順で実装することにします。

  1. デフォルトではuserSubmitBtn.disabled = true;で送信ボタンを無効化しておく
  2. 各入力フィールドのバリデーションチェック用の変数activeName, activeEmail, activePasswordを定義する
  3. 上記変数に対し、デフォルトではfalseを, バリデーションチェックをパスした場合はtrueを代入する
  4. 上記変数が全てtrueになった時に、userSubmitBtn.disabled = false;で送信ボタンを有効化する

上記の処理を行うコードを以下のように記述します。

<script>

 ・・・
  // 各要素のグローバル変数
 ・・・

  /**************************** ↓追記↓ *******************************/
 // 各入力フィールドのバリデーションチェック用の変数
  // エラー時は"false", OK時は"true"を代入する(後述)
  var activeName;
  var activeEmail;
  var activePassword;

  //Submitボタンの有効化条件
  userSubmitBtn.disabled = true; // デフォルトでSubmitボタンを無効化
  userForm.addEventListener('input', ()=>{
    if(activeName === true && activeEmail === true && activePassword === true){
      userSubmitBtn.disabled = false;
    }else{
      userSubmitBtn.disabled = true;
    }
  });
  /******************************************************************/

  //表示名のバリデーション
  userName.addEventListener("input", ()=>{
    // イベント発火時の処理
  });

  //メールアドレスのバリデーション
  userEmail.addEventListener("input", ()=>{
    // イベント発火時の処理
  });

  //パスワードのバリデーション
  userPasswordGroup.addEventListener("input", ()=>{
    // イベント発火時の処理
  });

</script>

これで、バリデーションチェックが全てパスした場合は送信ボタンが有効化され、逆に1つでもエラーがある場合は送信ボタンが無効化されるようになりました。

if文でバリデーション処理の内容を記述

それでは、userName, userEmail, userPasswordGroupに対してバリデーションを実行するイベント詳細を記述していきましょう。

表示名(:name)のバリデーション

今回、:nameにおけるバリデーションエラーの発動条件は以下のように設定しました。

  • 入力フォームが空の場合
  • 入力した表示名が3文字未満の場合
  • 入力した表示名が50文字を超える

これをjsコードで記述すると以下のようになります。

<script>
 ・・・
  //表示名(:name)のバリデーション
  userName.addEventListener('input', ()=>{
    if(userName.value === ""){ // 入力フォームが空の場合
      namePara.textContent = "表示名を入力してください";
      namePara.style.color = "red";
      userName.parentElement.style.border = "2px solid red";
      activeName = false;
    }else if(userName.value.length < 3){ // 文字数が3文字未満の場合
      namePara.textContent = "表示名は3文字以上で入力してください";
      namePara.style.color = "red";
      userName.parentElement.style.border = "2px solid red";
      activeName = false;
    }else if(userName.value.length > 50){ // 文字数が50文字を超える場合
      namePara.textContent = "表示名は50文字以内で入力してください";
      namePara.style.color = "red";
      userName.parentElement.style.border = "2px solid red";
      userSubmitActive -= 1;
      activeName = false;
    }else{ // バリデーションチェック完了時(OK時)の処理
      userName.parentElement.style.border = "2px solid lightgreen";
      namePara.textContent = "";
      activeName = true;
    }
    userNameField.appendChild(namePara);
  });
</script>

メールアドレス(:email)のバリデーション

:emailにおけるバリデーションエラーの発動条件は以下のように設定しました。

  • 入力フォームが空の場合
  • 入力したメールアドレスが正規表現のパターンにマッチしない場合(メールアドレスとして有効でない場合)

これをjsコードで記述すると以下のようになります。

<script>
 ・・・
  //表示名(:name)のバリデーション
  userName.addEventListener("input", ()=>{
    ・・・
  });

  //メールアドレス(:email)のバリデーション
  userEmail.addEventListener('input', ()=>{
    // email の正規表現パターン↓
    var emailRegex = /^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
    if(userEmail.value === ""){ // 入力フォームが空の場合
      emailPara.textContent = "メールアドレスを入力してください";
      emailPara.style.color = "red";
      userEmail.parentElement.style.border = "2px solid red";
      activeEmail = false;
    }else if(!emailRegex.test(userEmail.value)){ // 入力した値がemailRegexの正規表現パターンにマッチしない場合
      emailPara.textContent = "有効なアドレスを入力してください";
      emailPara.style.color = "red";
      userEmail.parentElement.style.border = "2px solid red";
      activeEmail = false;
    }else{ // バリデーションチェック完了時(OK時)の処理
      userEmail.parentElement.style.border = "2px solid lightgreen";
      emailPara.textContent = "";
      activeEmail = true;
    }
    userEmailField.appendChild(emailPara); // :email用のバリデーションエラーメッセージを表示
  });
</script>

パスワード(:password, :password_confirmation)のバリデーション

:passwordおよび:password_confirmationにおけるバリデーションエラーの発動条件は、:name:emailと違って少し複雑です。

下記のように、大きく2パターンに分けて考えていきます。

  1. 入力した:passwordが正規表現のパターンにマッチしない場合(半角英数字でない、または6文字未満)
    • :password入力フォームが空の場合
    • 入力した:passwordが1文字以上6文字未満の場合
  2. 入力した:passwordが正規表現のパターンにマッチした場合(半角英数字かつ6文字以上)
    • :password_confirmation入力フォームが空の場合
    • :passwordおよび:password_confirmationの値が一致しない場合

ここで、先ほどのフォーム画面全体像をもう一度確認してみましょう。

パスワードのバリデーションでは:password:password_confirmationの入力内容が一致するかどうかをチェックするので、入力フォームを2つ同時に監視することになります。

:name:emailではそれぞれイベントを追加していましたが、パスワードに関しては:password:password_confirmationをグループ化したuserPasswordGroupフィールドを作成し、

そのuserPasswordGroupフィールド全体に対してaddEventListenerでイベント追加する必要があります。

以上の内容を踏まえて、パスワードのバリデーションチェックをjsコードで記述すると以下のようになります。

<script>
 ・・・
  //表示名のバリデーション
  userName.addEventListener("input", ()=>{
    ・・・
  });

  //メールアドレスのバリデーション
  userEmail.addEventListener('input', ()=>{
    ・・・
  });

  //パスワード(:password, :password_confirmation)のバリデーション
  userPasswordGroup.addEventListener('input', ()=>{
    var passwordRegex = /^([a-zA-Z0-9]{6,})$/; // 半角英数字6文字以上を表す正規表現

    // パスワード(:password)がpasswordRegexの正規表現パターンに一致しない時の処理
    if(!passwordRegex.test(userPassword.value)){
      if(userPassword.value === ""){ // 入力フォーム(userPassword)が空の場合
        confirmationPara.textContent = "";
        passwordPara.textContent = "パスワードを入力してください";
        passwordPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid red";
        activePassword = false;
      }else{ // 入力した値が1文字以上6文字未満である場合
        confirmationPara.textContent = "";
        passwordPara.textContent = "6文字以上の半角英数字を入力してください";
        passwordPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid red";
        userPasswordConfirmation.parentElement.style.border = "none";
        activePassword = false;
      }
    }
    // パスワード(:password)がpasswordRegexの正規表現パターンに一致した時の処理
    else{
      if(userPasswordConfirmation.value === ""){ // 入力フォーム(userPasswordConfirmation)が空の場合
        passwordPara.textContent = "";
        confirmationPara.textContent = "パスワード(確認用)を入力してください";
        confirmationPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid lightgreen";
        userPasswordConfirmation.parentElement.style.border = "2px solid red";
        activePassword = false;
      }else if(userPasswordConfirmation.value === userPassword.value){ // パスワード、パスワード確認用の値が一致する場合
        passwordPara.textContent = "";
        confirmationPara.textContent = "";
        userPassword.parentElement.style.border = "2px solid lightgreen";
        userPasswordConfirmation.parentElement.style.border = "2px solid lightgreen";
        activePassword = true;
      }else{ // パスワード、パスワード確認用の値が一致しない場合
        passwordPara.textContent = "";
        confirmationPara.textContent = "入力したパスワードと一致しません";
        confirmationPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid lightgreen";
        userPasswordConfirmation.parentElement.style.border = "2px solid red";
        activePassword = false;
      }
    }

    userPasswordField.appendChild(passwordPara); // :password用のバリデーションエラーメッセージを表示
    userPasswordConfirmationField.appendChild(confirmationPara); // :password_confirmation用のバリデーションエラーメッセージを表示
  });

</script>

(一応)完成コード

当記事における(一応)完成形のviewファイルのコード、およびバリデーション用のJavaScriptコードをそれぞれ置いておきます。

こちらはviewファイルのコードです↓

<div class="row justify-content-center">
  <%= form_with(model: @user, local: true, class: "mt-4 form-column-width", id: "userForm") do |f| %>

    <!----- ユーザー名(:name) ------>
    <div class="mt-3 mb-3", id="userName">
      <%= f.label :name, class: "form-label fw-bold"%>
      <span class="text-danger">*</span>
      <div class="input-group shadow-sm rounded">
        <span class="input-group-text" id="basic-addon1">
          <i class="bi bi-person-circle"></i>
        </span>
        <%= f.text_field :name, placeholder: "ユーザー名を入力してください", class: "form-control"%>
      </div>
    </div>

    <!----- 性別 ------>
   ・・・

    <!----- メールアドレス(:email) ------>
    <div class="mb-3" id="userEmail">
      <%= f.label :email, class: "form-label fw-bold"%>
      <span class="text-danger">*</span>
      <div class="input-group shadow-sm rounded">
        <span class="input-group-text" id="basic-addon1">
          <i class="bi bi-envelope"></i>
        </span>
        <%= f.email_field :email, class: "form-control", placeholder: "Eメールアドレスを入力してください" %>
      </div>
      <div id="emailHelp" class="form-text text-primary">※アカウント認証確認メールを送信する為、必ず実際に使えるメールアドレスをご使用ください</div>
    </div>

    <!----- パスワードフォームのグループ(:password, :password_confirmation) ------>
    <div id="userPasswordGroup">
      <!----- パスワード ------>
      <div class="mb-3" id="userPassword">
        <%= f.label :password, class: "form-label fw-bold"%>
        <span class="text-danger">*</span>
        <div class="input-group shadow-sm rounded">
          <span class="input-group-text" id="basic-addon1">
            <i class="bi bi-key-fill"></i>
          </span>
          <%= f.password_field :password, class: "form-control", placeholder: "パスワードを入力してください" %>
        </div>
        <div id="emailHelp" class="form-text text-primary">※6文字以上の半角英数字を入力してください</div>
      </div>

      <!----- パスワード(確認用) ------>
      <div class="mb-3" id="userPasswordConfirmation">
        <%= f.label :password_confirmation, class: "form-label fw-bold"%>
        <span class="text-danger">*</span>
        <div class="input-group shadow-sm rounded">
          <span class="input-group-text" id="basic-addon1">
            <i class="bi bi-key-fill"></i>
          </span>
          <%= f.password_field :password_confirmation, class: "form-control", placeholder: "パスワードをもう一度入力してください" %>
        </div>
      </div>
    <% end %>
  </div>

  <!----- 送信(更新)ボタン ------>
  <%= f.submit class: "btn btn-primary mb-4", id: "userSubmit" %>

  <% end %>
</div>

<!----- バリデーション用の jsコード読み込み(_form_validation.html.erb) ------>
<%= render "form_validation" %>

こちらはJavaScriptコードです↓

<script>
  // フォーム全体の要素
  var userForm = document.querySelector("#userForm");

  // 各入力フィールド(name, email, password, password_confirmation) の要素
  var userNameField = document.querySelector('#userName');
  var userEmailField = document.querySelector('#userEmail');
  var userPasswordField = document.querySelector('#userPassword');
  var userPasswordConfirmationField = document.querySelector('#userPasswordConfirmation');

  // パスワードおよびパスワード確認用フィールドの要素グループ
  var userPasswordGroup = document.querySelector('#userPasswordGroup');

  // 各入力フィールド内の <input> タグ
  var userName = document.querySelector("#user_name");
  var userEmail = document.querySelector("#user_email");
  var userPassword = document.querySelector("#user_password");
  var userPasswordConfirmation = document.querySelector("#user_password_confirmation");

  // フォーム送信ボタン(Submit)の要素
  var userSubmitBtn = document.querySelector('#userSubmit');

  // 各フィールドのエラーメッセージ表示用の paragraph
  var namePara = document.createElement('p');
  var emailPara = document.createElement('p');
  var passwordPara = document.createElement('p');
  var confirmationPara = document.createElement('p');

  // 各入力フィールドのバリデーションチェック用の変数
  // エラー時は"false", OK時は"true"を代入する(後述)
  var activeName;
  var activeEmail;
  var activePassword;

  // Submitボタンの有効化条件
  userSubmitBtn.disabled = true; // デフォルトでSubmitボタンを無効化
  userForm.addEventListener('input', ()=>{
    if(activeName === true && activeEmail === true && activePassword === true){
      userSubmitBtn.disabled = false;
    }else{
      userSubmitBtn.disabled = true;
    }
   });

  //表示名(:name)のバリデーション
  userName.addEventListener('input', ()=>{
    if(userName.value === ""){ // 入力フォームが空の場合
      namePara.textContent = "表示名を入力してください";
      namePara.style.color = "red";
      userName.parentElement.style.border = "2px solid red";
      activeName = false;
    }else if(userName.value.length < 3){ // 文字数が3文字未満の場合
      namePara.textContent = "表示名は3文字以上で入力してください";
      namePara.style.color = "red";
      userName.parentElement.style.border = "2px solid red";
      activeName = false;
    }else if(userName.value.length > 50){ // 文字数が50文字以上の場合
      namePara.textContent = "表示名は50文字以内で入力してください";
      namePara.style.color = "red";
      userName.parentElement.style.border = "2px solid red";
      userSubmitActive -= 1;
      activeName = false;
    }else{ // バリデーションチェック完了時(OK時)の処理
      userName.parentElement.style.border = "2px solid lightgreen";
      namePara.textContent = "";
      activeName = true;
    }
    userNameField.appendChild(namePara); // :name用のバリデーションエラーメッセージを表示
  });

  //メールアドレス(:email)のバリデーション
  userEmail.addEventListener('input', ()=>{
    // email の正規表現パターン↓
    var emailRegex = /^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
    if(userEmail.value === ""){ // 入力フォームが空の場合
      emailPara.textContent = "メールアドレスを入力してください";
      emailPara.style.color = "red";
      userEmail.parentElement.style.border = "2px solid red";
      activeEmail = false;
    }else if(!emailRegex.test(userEmail.value)){ // 入力した値がemailRegexの正規表現パターンにマッチしない場合
      emailPara.textContent = "有効なアドレスを入力してください";
      emailPara.style.color = "red";
      userEmail.parentElement.style.border = "2px solid red";
      activeEmail = false;
    }else{ // バリデーションチェック完了時(OK時)の処理
      userEmail.parentElement.style.border = "2px solid lightgreen";
      emailPara.textContent = "";
      activeEmail = true;
    }
    userEmailField.appendChild(emailPara); // :email用のバリデーションエラーメッセージを表示
  });

  //パスワード(:password, :password_confirmation)のバリデーション
  userPasswordGroup.addEventListener('input', ()=>{
    var passwordRegex = /^([a-zA-Z0-9]{6,})$/; // 半角英数字6文字以上を表す正規表現

    // パスワード(:password)がpasswordRegexの正規表現パターンに一致しない時の処理
    if(!passwordRegex.test(userPassword.value)){
      if(userPassword.value === ""){ // 入力フォーム(userPassword)が空の場合
        confirmationPara.textContent = "";
        passwordPara.textContent = "パスワードを入力してください";
        passwordPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid red";
        activePassword = false;
      }else{ // 入力した値が1文字以上6文字未満である場合
        confirmationPara.textContent = "";
        passwordPara.textContent = "6文字以上の半角英数字を入力してください";
        passwordPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid red";
        userPasswordConfirmation.parentElement.style.border = "none";
        activePassword = false;
      }
    }

  //パスワード(:password, :password_confirmation)のバリデーション
  userPasswordGroup.addEventListener('input', ()=>{
    var passwordRegex = /^([a-zA-Z0-9]{6,})$/; // 半角英数字6文字以上を表す正規表現

    // パスワード(:password)がpasswordRegexの正規表現パターンに一致しない時の処理
    if(!passwordRegex.test(userPassword.value)){
      if(userPassword.value === ""){ // 入力フォーム(userPassword)が空の場合
        confirmationPara.textContent = "";
        passwordPara.textContent = "パスワードを入力してください";
        passwordPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid red";
        activePassword = false;
      }else{ // 入力した値が1文字以上6文字未満である場合
        confirmationPara.textContent = "";
        passwordPara.textContent = "6文字以上の半角英数字を入力してください";
        passwordPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid red";
        userPasswordConfirmation.parentElement.style.border = "none";
        activePassword = false;
      }
    }
    // パスワード(:password)がpasswordRegexの正規表現パターンに一致した時の処理
    else{
      if(userPasswordConfirmation.value === ""){ // 入力フォーム(userPasswordConfirmation)が空の場合
        passwordPara.textContent = "";
        confirmationPara.textContent = "パスワード(確認用)を入力してください";
        confirmationPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid lightgreen";
        userPasswordConfirmation.parentElement.style.border = "2px solid red";
        activePassword = false;
      }else if(userPasswordConfirmation.value === userPassword.value){ // パスワード、パスワード確認用の値が一致する場合
        passwordPara.textContent = "";
        confirmationPara.textContent = "";
        userPassword.parentElement.style.border = "2px solid lightgreen";
        userPasswordConfirmation.parentElement.style.border = "2px solid lightgreen";
        activePassword = true;
      }else{ // パスワード、パスワード確認用の値が一致しない場合
        passwordPara.textContent = "";
        confirmationPara.textContent = "入力したパスワードと一致しません";
        confirmationPara.style.color = "red";
        userPassword.parentElement.style.border = "2px solid lightgreen";
        userPasswordConfirmation.parentElement.style.border = "2px solid red";
        activePassword = false;
      }
    }

    userPasswordField.appendChild(passwordPara); // :password用のバリデーションエラーメッセージを表示
    userPasswordConfirmationField.appendChild(confirmationPara); // :password_confirmation用のバリデーションエラーメッセージを表示
  });

</script>

JavaScript初心者ということもあり少しコードが冗長(非効率)かもしれませんがご容赦を。

(もしかしたら、見えないバグがある可能性も。見つけたら修正します)

今回書いたコードをいかにコンパクトに効率よくまとめるかが今後の課題ですね。

レベルアップしたらまたリファクタリングして更新しようと思います。

【Rails7.0以降】Turbo Drive有効時のjs内でlet, constを使うとエラーになる問題

今どきのモダンなJavaScript(以下js)では、変数の宣言にvarではなくletを使うことが推奨されています。

【参考】varとletの違い

しかし、今回のjsのコードでは、あえて全ての変数をvarで宣言しました。(本当なら変数はlet、定数はconstで宣言したかったのですが…)

これには理由がありまして…

もし、今回のjs実装で以下のように変数(もしくは定数)を定義したとします。

<script>
  let userForm = document.querySelector("#userForm");
  // or
  const userForm = document.querySelector("#userForm");
  ・・・
</script>

このように宣言した後に、一度ページ(新規登録フォーム)を離れてから再び同じページに遷移すると、jsコンソールで以下のようなエラーが発生します。

Uncaught SyntaxError: Identifier ‘〇〇’ has already been declared

この変数(もしくは定数)はすでに宣言されていますよ!というエラーです。

jsではletconstは同じプログラム内で再宣言することができません。

では、なぜ変数や定数が再宣言されてしまうのでしょうか。

その理由はTurbo Driveが有効化されているからです(Rails7ではデフォルトでTrubo Driveが有効化されている)。

Turbo Driveが有効化されていると、letで宣言した変数(constで宣言した定数)はページ遷移をしても保持される、かつ一度訪れたページに再訪問するとjsプログラムが再度発火して同じ名前の変数(もしくは定数)を再宣言しようとするために起こります。

そこで、同じ変数を何度も再宣言できるvarを使うことでこの問題を解決することができました。

ただ、varを使うことはあまり推奨されていないので、今後はletconstを使ってもエラーがでない解決法があれば追記しようと思います。

【追記】
解決策見つかりました。JS + Stimulus で動的バリデーションチェックを実装してみたらコード量も3分の2くらいまで減らすことができ、さらに変数宣言の問題も解消されました。Rails7以降で開発するなら個人的にStimulusかなりおすすめです。
【Rails7】JavaScript + Stimulusで動的なバリデーションチェックを実装してみた

参考資料

MDN Web Docs
イベント入門 - ウェブ開発を学ぶ | MDN イベントは、あなたがプログラミングしているシステムで起こることで、あなたのコードがそれらに反応できるようにシステムがあなたに伝えるものです。
Qiita
Rails + Turbolinks上のjs内でletを使うと怒られる(Identifier 'a' has already been declared) - Qiita turbolinks × let の相性?かなり時間を取られたのでメモ。<script> let a = "Hello";</script>このようにerbのscriptタグ内にletで変数…
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次