【Rails7】JavaScript + Stimulusで動的なバリデーションチェックを実装してみた

以前、JavaScriptで動的なバリデーションチェックを実装する旨の記事を書きましたが、JSのコードが冗長なのと変数宣言にvarしか使えない(Turboとの相性により)という課題がありました。

あわせて読みたい
【Rails】JavaScriptで動的なバリデーション(リアルタイムでチェックする機能)を実装してみた Railsでフォームのバリデーションチェックを行う場合、送信ボタンを押した後でないとエラーがあるかどうかを判定できません。 バックエンド側でバリデーションチェック...

しかし、Rails7にはStimulusという素晴らしいJSライブラリがあるではありませんか!

このStimulusとJSを組み合わせることにより、課題だったコードの冗長性と変数宣言の問題を一挙に解決できることが判明。

ということで、今回はJavaScript + Stimulus で動的バリデーションチェックを実装する方法について解説していきます。

目次

開発環境

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

出来上がりイメージ

出来上がりイメージは前回の記事で実装したもの(ユーザー新規登録画面のバリデーションチェック)とほぼ同じです↓

チェック項目は以下の通り。

  • ユーザー名は値の有無をチェック
  • Eメールアドレスは値の有無と正規表現のパターンにマッチするかどうかチェック
  • パスワードは値の有無と正気表現のパターンにマッチするかどうかチェック
  • パスワードとパスワード(確認用)が一致しているかどうかチェック
  • 全てのチェックをパスしたら「登録する」ボタンを有効化する

これらを、今回はJavaScript + Stimulusで実装していきます。

用意するファイルはフォーム用のviewファイルとStimulusコントローラーの2種類です。

コードの完成形

以下、先にコードの完成形を載せておきます(コードの全体像を掴むために)。

まずはユーザー新規登録用フォームのviewファイル_form.html.erbから↓

= form_with(model: @user, data: { controller: "signup", 
                                      action: "input->signup#validSubmit" }) do |f| 

  #------- ユーザー名 --------
  <div class="mb-3"> # cssフレームワーク は bootstrap5 を使用
    = f.label "ユーザー名(表示名)", class: "form-label required" 
    = f.text_field :name, class: "form-control", 
                           data: { signup_target: "name", 
                                          action: "input->signup#nameValidation" 
                                 } 
    <p data-signup-target="error_name"></p> # ←エラーメッセージ表示用の要素
  </div>

  #------- Eメールアドレス --------
  <div class="mb-3">
    = f.label "Eメールアドレス", class: "form-label required"
    = f.email_field :email, class: "form-control", 
                             data: { signup_target: "email", 
                                            action: "input->signup#emailValidation" 
                                   } 
      <p data-signup-target="error_email"></p> # ←エラーメッセージ表示用の要素
  </div>

  #------- パスワード & パスワード確認 --------
  <div data-action="input->signup#passwordValidation">
    <div class="mb-3">
      = f.label "パスワード", class: "form-label required"
      = f.password_field :password, class: "form-control", data: { signup_target: "password" }
      <p data-signup-target="error_password"></p> # ←エラーメッセージ表示用の要素
    </div>

    <div class="mb-3">
      = f.label "パスワード(確認用)", class: "form-label required"
      = f.password_field :password_confirmation, class: "form-control", data: { signup_target: "password_confirmation" }
      <p data-signup-target="error_password_confirmation"></p> # ←エラーメッセージ表示用の要素
    </div>
  </div>

  #------- 送信 --------%>
  <div class="mb-5">
    # ↓送信ボタンはデフォルトで無効(disabled: true)にしておく
    = f.submit class: "btn btn-primary", disabled: true, data: { signup_target: "submit" }
    = link_to "キャンセル", root_path, class: "btn btn-secondary"
  </div>

続いて、バリデーション用のJSを記述するStimulusコントローラーsignup_controller.jsのコードの全体像です↓

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="signup"
export default class extends Controller {
  static targets = [
                    "name",
                    "email",
                    "password",
                    "password_confirmation",
                    "submit",
                    "error_name",
                    "error_email",
                    "error_password",
                    "error_password_confirmation"
                    ]

  // ユーザー名(表示名)のバリデーション
  nameValidation() {
    const nameInput = this.nameTarget // ユーザー名の input
    const nameError = this.error_nameTarget // エラーメッセージ
    if(nameInput.value === ""){ // 入力フォームが空の場合
      nameInput.style.border = "2px solid red"
      nameError.textContent = "ユーザー名を入力してください"
      nameError.style.color = "red"
    }else{
      nameInput.style.border = "2px solid lightgreen"
      nameError.textContent = ""
    }
  }

  // Eメールアドレスのバリデーション
  emailValidation() {
    const emailInput = this.emailTarget // Eメールアドレスの input
    const emailError = this.error_emailTarget // エラーメッセージ
    const 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(emailInput.value === ""){ // 入力フォームが空の場合
      emailInput.style.border = "2px solid red"
      emailError.textContent = "Eメールアドレスを入力してください"
      emailError.style.color = "red"
    }else if(!emailRegex.test(emailInput.value)){ // 入力した値がemailRegexの正規表現パターンにマッチしない場合
      emailInput.style.border = "2px solid red"
      emailError.textContent = "有効なEメールアドレスを入力してください"
      emailError.style.color = "red"
    }else{ // 正規表現パターンにマッチする場合
      emailInput.style.border = "2px solid lightgreen"
      emailError.textContent = ""
    }

  // パスワードのバリデーション
  passwordValidation() {
    const passInput = this.passwordTarget // パスワードの input
    const passConfirmInput = this.password_confirmationTarget // パスワード(確認用)の input
    const passError = this.error_passwordTarget // エラーメッセージ
    const passConfirmError = this.error_password_confirmationTarget // エラーメッセージ
    const passRegex = /^([a-zA-Z0-9]{6,})$/ // 正規表現パターン(半角英数字6文字以上)

    // パスワードがpassRegexの正規表現パターンに一致しない時の処理
    if(!passRegex.test(passInput.value)){
      if(passInput.value === ""){ // 入力フォームが空の場合
        passInput.style.border = "2px solid red"
        passError.textContent = "パスワードを入力してください"
        passError.style.color = "red"
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = ""
      }else{
        passInput.style.border = "2px solid red"
        passError.textContent = "6文字以上の半角英数字で入力してください"
        passError.style.color = "red"
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = ""
      }
    }else{ // 正規表現パターンに一致した時の処理
      if(passConfirmInput.value === ""){ // 入力フォームが空の場合
        passInput.style.border = "2px solid lightgreen"
        passError.textContent = ""
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = "パスワード(確認用)を入力してください"
        passConfirmError.style.color = "red"
      }else if(passInput.value === passConfirmInput.value){ // パスワード、パスワード確認用の値が一致する場合
        passInput.style.border = "2px solid lightgreen"
        passError.textContent = ""
        passConfirmInput.style.border = "2px solid lightgreen"
        passConfirmError.textContent = ""
      }else{ // パスワード、パスワード確認用の値が一致しない場合
        passInput.style.border = "2px solid lightgreen"
        passError.textContent = ""
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = "入力したパスワードと一致しません"
        passConfirmError.style.color = "red"
      }
    }
  }

// 送信ボタンの有効化
  validSubmit() {
    const submitBtn = this.submitTarget // 送信ボタン

    if((this.nameTarget.value !== "") && (this.emailTarget.value !== "") 
    && (this.passwordTarget.value !== "") 
    && (this.password_confirmationTarget.value !== "")){ // 全ての入力フォームに値が存在する、且つ ↓
      if((this.error_nameTarget.textContent === "") && (this.error_emailTarget.textContent === "") 
     && (this.error_passwordTarget.textContent === "") 
     && (this.error_password_confirmationTarget.textContent === "")){ // エラーメッセージが全てなくなった場合
        submitBtn.disabled = false // 送信ボタンを有効化
      }else{
        submitBtn.disabled = true
      }
    }else{
      submitBtn.disabled = true
    }
  }
}

それでは、順番に説明していきます。

JS(Stimulus)で動的なバリデーションチェックを実装する流れ

入力フォームの大枠を作成する

= form_with(model: @user) do |f| 

  #------- ユーザー名 --------
  <div class="mb-3"> # cssフレームワーク は bootstrap5 を使用
    = f.label "ユーザー名(表示名)", class: "form-label required" 
    = f.text_field :name, class: "form-control"
    <p></p> # ←エラーメッセージ表示用の要素
  </div>

  #------- Eメールアドレス --------
  <div class="mb-3">
    = f.label "Eメールアドレス", class: "form-label required"
    = f.email_field :email, class: "form-control"
      <p></p> # ←エラーメッセージ表示用の要素
  </div>

  #------- パスワード & パスワード確認 --------
    <div class="mb-3">
      = f.label "パスワード", class: "form-label required"
      = f.password_field :password, class: "form-control"
      <p></p> # ←エラーメッセージ表示用の要素
    </div>

    <div class="mb-3">
      = f.label "パスワード(確認用)", class: "form-label required"
      = f.password_field :password_confirmation, class: "form-control"
      <p></p> # ←エラーメッセージ表示用の要素
    </div>

  #------- 送信 --------%>
  <div class="mb-5">
    # ↓送信ボタンはデフォルトで無効(disabled: true)にしておく
    = f.submit class: "btn btn-primary", disabled: true
    = link_to "キャンセル", root_path, class: "btn btn-secondary"
  </div>

data属性【controller】【target】【action】をフォーム(html.erb)に追加する

StimulusコントローラーからHTMLファイル(先ほど作成したフォーム)にアクセスするためにはdata属性を設定する必要があります。

設定するdata属性は以下の3つです。

  1. data-controller
  2. data-action
  3. data-{controller}-target

まず、data-controllerStimulusコントローラーの適用範囲を設定します。

例えば、signup_controller.jsというコントローラーをHTMLに適用させたい場合、以下のように記述します。

<main>
  <div data-controller="signup">
    <h1>Sample1</h1>
    <h2>Sample2</h2>
  </div>
</main>

上記の場合、<div>〜</div>の範囲内、つまり<h1><h2>がコントローラーの適用範囲となります。

後ほど設定するdata-actionおよびdata-{controller}-targetは、data-controllerの適用範囲内で指定する必要があります。

続いてはdata-actionの設定です。

data-action属性を設定することで、設定した要素のイベントに対する振る舞い(アクション)を呼び出すことができます。

例えば、ボタンをクリックした時にuserコントローラーのsomeActionを実行したい場合は、ボタン要素に以下のように追記します。

<!--  click(クリックした時)->user(コントローラー)#someAction(アクション) を実行  -->
<button data-action="click->user#someAction"></button>

最後に、Stimulusコントローラー側で取得したいHTML要素(input)をdata-{controller}-targetで指定します。

StimulusではHTML側にdata-{controller}-target="{name}"属性を付与し、値に設定しておいた{name}をJS側の静的プロパティstaticに設定すると、{name}Targetと指定するだけで{name}の要素にアクセスできるようになります。

上記を踏まえて、data属性を以下のように設定します。

= form_with(model: @user, data: { controller: "signup", 
                                      action: "input->signup#validSubmit" }) do |f| 

  #------- ユーザー名 --------
  <div class="mb-3"> # cssフレームワーク は bootstrap5 を使用
    = f.label "ユーザー名(表示名)", class: "form-label required" 
    = f.text_field :name, class: "form-control", 
                           data: { signup_target: "name", 
                                          action: "input->signup#nameValidation" 
                                 } 
    <p data-signup-target="error_name"></p> # ←エラーメッセージ表示用の要素
  </div>

  #------- Eメールアドレス --------
  <div class="mb-3">
    = f.label "Eメールアドレス", class: "form-label required"
    = f.email_field :email, class: "form-control", 
                             data: { signup_target: "email", 
                                            action: "input->signup#emailValidation" 
                                   } 
      <p data-signup-target="error_email"></p> # ←エラーメッセージ表示用の要素
  </div>

  #------- パスワード & パスワード確認 --------
  <div data-action="input->signup#passwordValidation">
    <div class="mb-3">
      = f.label "パスワード", class: "form-label required"
      = f.password_field :password, class: "form-control", data: { signup_target: "password" }
      <p data-signup-target="error_password"></p> # ←エラーメッセージ表示用の要素
    </div>

    <div class="mb-3">
      = f.label "パスワード(確認用)", class: "form-label required"
      = f.password_field :password_confirmation, class: "form-control", data: { signup_target: "password_confirmation" }
      <p data-signup-target="error_password_confirmation"></p> # ←エラーメッセージ表示用の要素
    </div>
  </div>

  #------- 送信 --------%>
  <div class="mb-5">
    # ↓送信ボタンはデフォルトで無効(disabled: true)にしておく
    = f.submit class: "btn btn-primary", disabled: true, data: { signup_target: "submit" }
    = link_to "キャンセル", root_path, class: "btn btn-secondary"
  </div>

Stimulus コントローラーを追加する

以下のコマンドを実行してstimulusコントローラーを追加します。

(コントローラー名はsignupとしています)

$ rails g stimulus signup

app/javascript/controlles以下にsignup_controller.jsが追加されていればOKです。

Stimulusコントローラーに静的プロパティ(static targets)を定義する

追加したsignup_controller.jsを開いて、HTMLファイルに設定したdata-signup-target属性を静的プロパティ(static targets)として定義します。

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="signup"
export default class extends Controller {
  // viewファイルに設定したdata-signup-target属性
  static targets = [
                    "name",
                    "email",
                    "password",
                    "password_confirmation",
                    "submit",
                    "error_name",
                    "error_email",
                    "error_password",
                    "error_password_confirmation"
                    ]
}

静的プロパティを定義しておくことで、コントローラーからHTML要素へのアクセスが簡単に行えるようになります。

data-signup-target属性を”email”と設定した場合、this.emailTargetと記述するだけで”email”に設定した要素を取得できる)

Stimulusコントローラーにバリデーションチェックを行うためのアクションを記述する

続けて、signup_controller.jsにバリデーションチェックを行うためのアクションを記述します。

ユーザー名、Eメールアドレス、パスワードの各バリデーションチェックの内容は以下の通りです。

  • ユーザー名
    • 入力の有無をチェックする
  • Eメールアドレス
    • 入力の有無をチェックする
    • 正規表現パターンにマッチするかどうかをチェックする
  • パスワード
    • 入力の有無をチェックする
    • 正規表現パターンにマッチするかどうかをチェックする
    • パスワード(確認用)と一致するかどうかチェックする

これをコードで記述していきます↓

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="signup"
export default class extends Controller {
  static targets = [
                    "name",
                    "email",
                    "password",
                    "password_confirmation",
                    "submit",
                    "error_name",
                    "error_email",
                    "error_password",
                    "error_password_confirmation"
                    ]

  // ユーザー名(表示名)のバリデーション
  nameValidation() {
    const nameInput = this.nameTarget // ユーザー名の input
    const nameError = this.error_nameTarget // エラーメッセージ
    if(nameInput.value === ""){ // 入力フォームが空の場合
      nameInput.style.border = "2px solid red"
      nameError.textContent = "ユーザー名を入力してください"
      nameError.style.color = "red"
    }else{
      nameInput.style.border = "2px solid lightgreen"
      nameError.textContent = ""
    }
  }

  // Eメールアドレスのバリデーション
  emailValidation() {
    const emailInput = this.emailTarget // Eメールアドレスの input
    const emailError = this.error_emailTarget // エラーメッセージ
    const 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(emailInput.value === ""){ // 入力フォームが空の場合
      emailInput.style.border = "2px solid red"
      emailError.textContent = "Eメールアドレスを入力してください"
      emailError.style.color = "red"
    }else if(!emailRegex.test(emailInput.value)){ // 入力した値がemailRegexの正規表現パターンにマッチしない場合
      emailInput.style.border = "2px solid red"
      emailError.textContent = "有効なEメールアドレスを入力してください"
      emailError.style.color = "red"
    }else{ // 正規表現パターンにマッチする場合
      emailInput.style.border = "2px solid lightgreen"
      emailError.textContent = ""
    }

  // パスワードのバリデーション
  passwordValidation() {
    const passInput = this.passwordTarget // パスワードの input
    const passConfirmInput = this.password_confirmationTarget // パスワード(確認用)の input
    const passError = this.error_passwordTarget // エラーメッセージ
    const passConfirmError = this.error_password_confirmationTarget // エラーメッセージ
    const passRegex = /^([a-zA-Z0-9]{6,})$/ // 正規表現パターン(半角英数字6文字以上)

    // パスワードがpassRegexの正規表現パターンに一致しない時の処理
    if(!passRegex.test(passInput.value)){
      if(passInput.value === ""){ // 入力フォームが空の場合
        passInput.style.border = "2px solid red"
        passError.textContent = "パスワードを入力してください"
        passError.style.color = "red"
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = ""
      }else{
        passInput.style.border = "2px solid red"
        passError.textContent = "6文字以上の半角英数字で入力してください"
        passError.style.color = "red"
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = ""
      }
    }else{ // 正規表現パターンに一致した時の処理
      if(passConfirmInput.value === ""){ // 入力フォームが空の場合
        passInput.style.border = "2px solid lightgreen"
        passError.textContent = ""
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = "パスワード(確認用)を入力してください"
        passConfirmError.style.color = "red"
      }else if(passInput.value === passConfirmInput.value){ // パスワード、パスワード確認用の値が一致する場合
        passInput.style.border = "2px solid lightgreen"
        passError.textContent = ""
        passConfirmInput.style.border = "2px solid lightgreen"
        passConfirmError.textContent = ""
      }else{ // パスワード、パスワード確認用の値が一致しない場合
        passInput.style.border = "2px solid lightgreen"
        passError.textContent = ""
        passConfirmInput.style.border = "2px solid red"
        passConfirmError.textContent = "入力したパスワードと一致しません"
        passConfirmError.style.color = "red"
      }
    }
  }

}

送信ボタンの有効化・無効化

上記の内容でバリデーションチェックはほぼ完成ですが、あともう一つ「送信ボタン(登録ボタン)」の有効化・無効化を判定するアクションも付け加えていきます。

送信ボタンの有効化・無効化を判定する材料は以下の2つ。

  • 全てのフォームに値が入力されているかどうか
  • 全てのフォームのエラーメッセージが解消されているかどうか

これらをチェックし、送信ボタンの有効化・無効化を切り替えるようにします。

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="signup"
export default class extends Controller {
  static targets = [
                    "name",
                    "email",
                    "password",
                    "password_confirmation",
                    "submit",
                    "error_name",
                    "error_email",
                    "error_password",
                    "error_password_confirmation"
                    ]

 ・・・ (中略) ・・・

// 送信ボタンの有効化・無効化を判定するアクション
  validSubmit() {
    const submitBtn = this.submitTarget // 送信ボタン

    if((this.nameTarget.value !== "") && (this.emailTarget.value !== "") 
    && (this.passwordTarget.value !== "") 
    && (this.password_confirmationTarget.value !== "")){ // 全ての入力フォームに値が存在する、且つ ↓
      if((this.error_nameTarget.textContent === "") && (this.error_emailTarget.textContent === "") 
     && (this.error_passwordTarget.textContent === "") 
     && (this.error_password_confirmationTarget.textContent === "")){ // エラーメッセージが全てなくなった場合
        submitBtn.disabled = false // 送信ボタンを有効化
      }else{
        submitBtn.disabled = true
      }
    }else{
      submitBtn.disabled = true
    }
  }

}

上記の記述により、判定によって送信ボタンの有効化、無効化が切り替わるようになりました。

これで、JavaScript + Stimulus による動的バリデーションチェックの実装は完了です。

以前の記事でご紹介したバリデーションチェックに比べて、コード量を3分の2らいまで減らすことができました。

Stimulus、思ったよりも使いやすいしコード管理もしやすいので、今後フロント周りの実装はJS + Stimulus をガンガン使っていこうと思います。

以上です。

【追記】メールアドレスが登録済みかどうかをリアルタイムでチェックする方法

Ajaxを使ってメールアドレスが登録済みかどうかをリアルタイムで確認できる機能を追加しました。

当記事と併せて参考にしてみてください。

あわせて読みたい
【Rails7】Ajax + Stimulusでメールアドレスが登録済みかどうかチェックする方法 (jQueryなしで実装) Railsではフォーム入力時(新規ユーザー登録時)にすでに登録済みのメールアドレスがある場合、バリデーションが発動して登録できないようになっているかと思います。 ...

参考資料

Qiita
stimulus、初歩の初歩(基本構造についての私的理解まとめ) - Qiita こちらの続き、railsプロジェクトに当てる前提でいろいろ試しています。とりあえずハンドブックを読んで、自分なりに纏めてみる(ちなみに書いている人間はWebプログラミン...
Qiita
Stimulusをはじめよう - Qiita Stimulusとは何かStimulusとは、JavaScriptで書かれたクライアントサイドのライブラリです。Basecampによって開発され、2021/12にversion2がリリースされま…
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

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

コメント

コメントする

目次