【Rails7】Ajax + Stimulusでメールアドレスが登録済みかどうかチェックする方法 (jQueryなしで実装)

Railsではフォーム入力時(新規ユーザー登録時)にすでに登録済みのメールアドレスがある場合、バリデーションが発動して登録できないようになっているかと思います。

しかし、しかしですよ!?

メールアドレスがすでに登録されているかどうかは、フォームの入力を全て終えて「登録する」ボタンを押すまでは分かりません。

せっかく重い腰を上げて新規登録フォームを埋めたのに、バリデーションに引っかかったせいでまたフォームの入力し直して「登録する」ボタンを押さなきゃいけない…

なんて面倒なんだ…ファッ◯◯

(初見のサイトでメールアドレスが被ることはまずないと思いますが、アカウント名の場合なら「すでに存在するアカウントです」という場面がありうるかと…)

ユーザー側からしたら、メールアドレスをフォームに入力した瞬間に登録済みかどうか知りたいものですよね。

そこで、今回はメールアドレスが登録済みかどうかをリアルタイムでチェックする機構を実装してみたので、その実装方法について解説したいと思います。

ネット検索するとjQueryを使った実装例が多かったですが、僕個人的には脱jQueryしたい派なのでjQueryなしで実装してみました。

目次

開発環境

  • 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 使用

出来上がりイメージ

出来上がりイメージは以下の通りです↓

入力したメールアドレスに対し、「メールアドレスが登録済みかどうか」をリアルタイムでチェックしています。

(登録済みの場合は「すでに登録されているメールアドレスです」と表示)

最近のWebサービスではよく見かけますよね。

今回はこちらを、Ajax + Stimulus(JSライブラリ)を駆使して実装していきます。

前提条件

  • すでに新規登録フォームの基盤ができている(userモデル、コントローラー、view作成済み)
  • JSの動的バリデーションチェック実装済みこちらの記事を参照

当記事では、すでに実装済みの動的バリデーションチェックを改良する形で説明を進めていきます。

わからない場合は以下の記事を先に読んでおくことをお勧めします。

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

Ajax + Stimulus でEメールアドレスが登録済みかどうかをチェック(判定)する流れ

今回実装するプログラムの、大まかな処理の流れは以下の通りです。

  1. フォームに入力したEメールアドレスをJS(fetchメソッド)でusersコントローラーに渡す(JSのinputイベントで入力を検知するとfetchメソッドでPOST送信)
  2. usersコントローラー側で、受け取ったEメールアドレス情報からDBに該当するユーザーが存在するか確認(別途メソッドを追加)
  3. 該当するユーザーの存在の有無をJS側にレスポンスする
  4. レスポンスにユーザー情報が含まれる場合はフロント側で「すでに登録済みです」という旨を表示する

要は、フォームに入力したEメールアドレスをJS側(Stimulus)でusersコントローラーに送信(POSTリクエスト)し、

usersコントローラー側では受け取ったEメールアドレスを元にデータベース検索をかけ、該当するユーザーが存在するかどうかをレスポンスとして返す、

そのレスポンス結果をJS側で受け取り、if文で判定を下す、

といった流れです。

それでは順番に参ります。

usersコントローラーにメールアドレス登録済みかどうかを判定するアクションを追加

まずは、usersコントローラーにメールアドレス登録済みかどうかを判定するis_registered?アクションを追加します。

class UsersController < ApplicationController

  ・・・

  # メールアドレスがすでに登録されているか(ユーザーが存在するか)どうかチェックする
  def is_registered?
    user = User.find_by(email: params[:email]) # クリエパラメータに埋め込んだEメールアドレスからユーザーを検索(いなければnull)
    render json: user
  end

end

usersコントローラーではJS側からparams[:email]でEメールアドレスを取得し、データベース検索をかけてローカル変数userに代入します(ユーザーが存在しない場合はnullが代入される)。

そして、render json: useruserをJSON形式で返すようにしています。

JSON形式でレンダリングされたuserは後ほどJS側でresponseとして受け取ります。

ルーティングの設定

is_registered?アクションを実行するためのルーティングを設定します。

今回はJS側からクリエパラメータでEメールアドレス情報を送信するため、メソッドはPOSTとします。

post "signup/check_email", to: "users#is_registered?"
Prefix               Verb    URI Pattern                      Controller#Action
signup_check_email   POST   /signup/check_email(.:format)     users#is_registered?

フォームに入力したEメールアドレスをAjaxでusersコントローラー側に受け渡す(Stimulusコントローラー)

続いては、Stimulusコントローラーsignup_controller.jsに、入力したEメールアドレスをusersコントローラーに受け渡すよう記述します。

記述内容は以下の通りです↓(後ほど順番に解説します)

// Eメールアドレスのバリデーション
emailValidation() { // inputイベントが発生すると以下の処理が実行される

  ・・・

  // ① //
  const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
  const options = { 
    method: "POST", // POSTメソッドを指定
    headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken // CSRFトークンを設定
          }
  }

  // ② //
  const params = { // 入力したEメールアドレスをパラメータオブジェクトに代入する
    email: emailInput.value
  }

 // ③ //
  const query_params = new URLSearchParams(params)  // オブジェクト形式のparamsをクエリ文字列に変換

 // ④ //
  fetch("/signup/check_email" + "?" + query_params, options)
  // →http://localhost:3000/signup/check_email?email={入力したEメールアドレス} に POST送信

}

まず、①ではCSRF対策としてCSRFトークンを取得し、optionsオブジェクトに設定します。(後ほどfetchメソッドでPOST送信します)

なぜ、CSRFトークンを取得する必要があるのかと言いますと、Railsアプリケーションは外部からPOST送信しようとすると、CSRF対策のために以下のエラーが出ます。

(ActionController::InvalidAuthenticityToken (Can’t verify CSRF token authenticity.):

通常、Railsアプリケーション内部でのPOST送信はCSRFトークンが自動生成されるため特に意識する必要はありませんでしたが、どうやらJavaScriptからAjaxでのPOST送信を行う場合はCSRFトークンが生成されない(送信されない)ために引っかかってしまうようです。

Qiita
外部からPOSTできない?RailsのCSRF対策をまとめてみた - Qiita Railsアプリケーションに対して、外からPOST送信しようとすると、422エラー・Can't verify CSRF token authenticityエラーが出ます。 これはRailsが自動で生成してくれるCS...
Qiita
isomorphic-fetch(jQuery以外)を使用してRailsのCSRFトークン検証への対応方法 - Qiita ReactのテストツールであるJestの使い方を調べようとこちらの記事のソースに非同期処理を入れました。 その過程で(isomorphic)fetchを使用してのRailsのCSRF検証のパス方法...

②では入力したEメールアドレスをパラメータオブジェクトに代入します。

③では、URLSearchParams()メソッドを用いて②のオブジェクト形式のparamsをクエリ文字列に変換します。

// ③ //
const query_params = new URLSearchParams(params)

// 例)
const query_params1 = new URLSearchParams({"foo" : 1 , "bar" : 2});
query_params1.toString();
// "foo=1&bar=2"

const query_params2 = new URLSearchParams({"email" : "info@example.com"});
query_params2.toString();
// "email=1info%40example.com" ←(エスケープ処理により @ が %40 に変換される)

そして④では、fetchメソッドを用いて③でクエリ文字列に変換したEメールアドレスをクエリパラメータに代入し、設定したURLに対してPOST送信します。

// ④ //
fetch("/signup/check_email" + "?" + query_params, options)
// →http://localhost:3000/signup/check_email?email={入力したEメールアドレス} に POST送信

これにより、フォームに入力した値(Eメールアドレス)をusersコントローラーに受け渡すと同時に、usersコントローラーのis_registered?アクションが実行されます。

usersコントローラーからのレスポンスでEメールアドレスの有無を判定する(Stimulusコントローラー)

usersコントローラーのis_registered?アクションが実行された後の処理として、JS側に以下の処理を追記します(下の方)。

// Eメールアドレスのバリデーション
emailValidation() {

  ・・・

  const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
  const options = { 
    method: "POST", // POSTメソッドを指定
    headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken // CSRFトークンを設定
          }
  }

  const params = { // 入力したEメールアドレスをパラメータオブジェクトに代入する
    email: emailInput.value
  }

  const query_params = new URLSearchParams(params)  // オブジェクト形式のparamsをクエリ文字列に変換

  // users#is_registered?アクション に POSTリクエスト(クリエパラメータでemail情報の送信)
  fetch("/signup/check_email" + "?" + query_params, options)
  /***** ↓ 追記(ここから)  *****/
    .then(response => response.json()) // レスポンスを JSON オブジェクトとしてパースする
    .then(response => {
      if (response) { 
          // レスポンスにユーザー情報が返ってきた場合の処理(すでにユーザーが登録済み場合)
      }else{
         // レスポンスにユーザー情報が返って来なかった場合の処理(ユーザーが未登録の場合)
      }
    })
  /******  ここまで  ******/
}

追記したコードは、usersコントローラー(is_registered?アクション)からのレスポンス結果を受け取り、そのレスポンス結果からメールアドレス登録済みかどうかを判定する、というものです。

ここで、usersコントローラーのis_registered?アクションを思い出してみましょう。

class UsersController < ApplicationController
  protect_from_forgery except: :is_registered? # ← 「InvalidAuthenticityToken」エラー対策

  ・・・

  # メールアドレスがすでに登録されているか(ユーザーが存在するか)どうかチェックする
  def is_registered?
    user = User.find_by(email: params[:email]) # クリエパラメータに埋め込んだEメールアドレスからユーザーを検索(いなければnull)
    render json: user
  end

end

fetch(" ... ")メソッドのPOST送信で/signup/check_emailへアクセスすると、usersコントローラーのis_registered?メソッドが発動し、params[:email]でクリエパラメータに添付されたEメールアドレスを取得します。

そして、取得したEメールアドレス情報からUser.find_byで該当するユーザーを検索し、ユーザーが存在する場合はrender json: userで該当するユーザー情報をJSON形式で返します(ユーザーが存在しない場合はnullを返す)。

このユーザー情報(もしくはnull)を、JS側のresponseにて受け取ります。

実際にconsole.log(response)responseの中身を見てみると以下のようになります。

つまり、このresponseの中身がnullかどうかをif文で判定してやれば、メールアドレスが登録済みかどうかをチェックすることができます。

以上を踏まえて、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() {
  ・・・
  }

  // 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{ // 正規表現パターンにマッチする場合
      const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
      const options = { 
        method: "POST", // POSTメソッドを指定
        headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken // CSRFトークンを設定
              }
      }
      const params = { // 入力したEメールアドレスをパラメータオブジェクトに代入する
        email: emailInput.value
    }
      const query_params = new URLSearchParams(params) // オブジェクト形式のparamsをクエリ文字列に変換
      // users#is_registered?アクション に POSTリクエスト(クリエパラメータでemail情報の送信)
      fetch("/signup/check_email" + "?" + query_params, options)
        .then(response => response.json())
        .then(response => {
          if (response) { // レスポンスにユーザー情報が返ってきた場合(すでにユーザーが存在する場合)
            emailError.textContent = 'すでに登録されているメールアドレスです'
            emailInput.style.border = "2px solid red"
            emailError.style.color = "red"
          }else{ // ユーザーが存在しない場合
            emailInput.style.border = "2px solid lightgreen"
            emailError.textContent = ""
          }
        })
    }
  }

  // パスワードのバリデーション
  passwordValidation() {
  ・・・
  }

  // 送信ボタンの有効化
  validSubmit() {
  ・・・
  }

これで実装はほぼ完了です。

リクエストの実行タイミングを遅らせる(リクエスト回数の削減)

最後の最後にもう一つ。

上記のコードのままだと、フォームに入力イベント(input)がある毎に、タイムラグなしでバリデーションチェックが発動します。

inputイベントは1文字入力・削除するごとに発生するため、その分だけStimulusコントローラーのemailValidation()アクションが実行される回数が多くなる、

つまり、fetchでのリクエスト(POST送信)回数が多くなることを意味します。

リクエスト回数が多ければ多いほどデータベース検索回数も増え、サーバーへの負担も大きくなります。

それに、見栄えもあまり良いとは言えませんしね(エラーメッセージの明滅、色の変化で目がチカチカするw)。

そこで、これらの負担を少しでも軽減する&ユーザービリティを改善するためにJSのsetTimeout()メソッドを用いてリクエストの実行タイミングを指定します。

具体的には、inputイベントが発生してから◯◯ms後にリクエストを実行する(遅延させる)、といった感じです。

追記するコードは以下の通り↓

  // Eメールアドレスのバリデーション
  emailValidation() {
    ・・・

    // セットされているTimeoutをクリアする
    clearTimeout(this.timeoutEmail)

    // 300ms後に処理Aを実行する
    this.timeoutEmail = setTimeout( ()=> {
    
    // 処理A

    }, 300) // ← タイマーを設定する(300ms後に処理Aを実行)
  }

setTimeout( ()=> { ... }, delay)で囲った部分(第一引数)に遅延実行させたい処理Aを記述し、delayに該当する箇所(第二引数)に遅延させる時間(ms)を設定します。

また、clearTimeout()メソッドを設置することにより、emailValidation()アクションを実行するたびにタイマーがリセットされるようにしています。

こうすることで、例えばタイマーを300msとした場合、

  • 300ms以内にフォームへの入力があれば再びタイマーが300msからスタートする(タイマーがリセットされるため)
  • 連続してフォームへの入力がある場合は、300msを満了しない限り処理Aは実行されない
  • フォームの入力を終えて300msを満了したら、処理Aを実行する

このようにフォームへの入力(inputイベント)回数に対して、処理Aを実行する回数を削減することができます。

実際にコードに適用すると以下のようになります↓

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() {
  ・・・
  }

  // 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,}$/ // 正規表現パターン

    // セットされているTimeoutをクリアする
    clearTimeout(this.timeoutEmail)

    // 300ms後に下記処理を実行する
    this.timeoutEmail = setTimeout( ()=> {
      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{ // 正規表現パターンにマッチする場合
        const params = { // 入力したEメールアドレスをパラメータオブジェクトに渡す
          email: emailInput.value
        }
        const options = { // POSTメソッドを指定
          method: "POST"
        }
        const query_params = new URLSearchParams(params) // オブジェクト形式のparamsをクエリ文字列に変換

        // users#is_registered?アクション に POSTリクエスト(クリエパラメータでemail情報の送信)
        fetch("/signup/check_email" + "?" + query_params, options)
          .then(response => response.json())
          .then(response => {
            if (response) { // レスポンスにユーザー情報が返ってきた場合(すでにユーザーが存在する場合)
              emailError.textContent = 'すでに登録されているメールアドレスです'
              emailInput.style.border = "2px solid red"
              emailError.style.color = "red"
            }else{ // ユーザーが存在しない場合
              emailInput.style.border = "2px solid lightgreen"
              emailError.textContent = ""
            }
          })
      }
    }, 300) // ← タイマーを設定する(300ms後に処理を実行)
  }

  // パスワードのバリデーション
  passwordValidation() {
  ・・・
  }

  // 送信ボタンの有効化
  validSubmit() {
  ・・・
  }

実際の動作は以下のようなイメージです↓

このように、連続して入力している間はバリデーションチェックの処理が行われず、入力を終えてから時間差でバリデーションチェック処理が行われるようになりました。

これで、AjaxによるPOST送信の回数やサーバーへの負担も減らせるし、目がチカチカしないし一石二鳥ですね。

以上です。

参考資料

Qiita
RailsとVue.jsでメールアドレスが登録済みか判定する方法 - Qiita こんばんは アロハな男、やすのりです! 今日は会員制のWebアプリ等でよく見かける『このメールアドレスは登録済みです。』みたいな表示をRailsとVue.jsを使って実装しち...
オブジェクト指向がわからない!
jQueryを使わないAjax、Fetch APIの書き方 | オブわか! 脱jQueryの勢いが増しております。 ちょっと前まで「まだjQuery使ってないの!?おっくれてるぅ〜」とか言っていた女子高生も、今では「エーッ!?まだjQueryなんか使って...
Qiita
fetchでクエリパラメータを渡す方法 - Qiita ajaxをfetch apiで書き変えた時に、getリクエストにクエリパラメータをつける方法がわからず詰まってしまったのでメモ。 fetchにはgetリクエストにクエリパラメータをつけ...
あわせて読みたい
フェッチ API の使用 - Web API | MDN フェッチ API は、リクエストやレスポンスといったプロトコルを操作する要素にアクセスするための JavaScript インターフェイスです。グローバルの fetch() メソッドも提供...
Stack Overflow
Rails: Can't verify CSRF token authenticity when making a POST request I want to make POST request to my local dev, like this: HTTParty.post('http://localhost:3000/fetch_heroku', :body => {:type => 'product...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

名古屋の34歳。国立大学院(情報工学)→自動車部品メーカー開発(3年)→NZワーホリ(1年3ヶ月)→ブロガー(最高月8万PV、月収25万円達成→コロナでアクセス数激減)兼フリーター(季節労働など5年)→無職のブロガー。学びや体験談をブログで発信することが好き。趣味は音ゲーとプログラミングと朝ラン。今のブログをより面白いコンテンツにするため、ネタ収集のためお仕事を募集してます。詳しくはこちら

コメント

コメントする

目次