Javascriptでバリデーションを実装

# はじめに
前回に引き続きbootstrapのモーダル機能で新規登録・ログインフォームを作成方法をアウトプットします✍️

https://qiita.com/3rarara/items/6450603f2ea29f5d7793

初心者ですので、もっといい方法があるよ！
等ございましたらご教示いただけますと幸いです！

# 開発環境
- ruby: 3.1.2
- Rails: 6.1.7.7
- Cloud9
- bootstrap: 4.6.2

# 前提条件
- bootstrap4.6.2 導入済み
- devise 導入済み

# Javascriptでバリデーションを実装
新規登録やログインにエラーが発生した時、
deviseの使用上元の(モーダルでない)新規登録ページやログインページに飛んでしまいます
ログインを非同期で行うことも試みましたが挫折しました…
なので、今回はJavascriptで簡単なバリデーションをつけていきたいと思います
コード全体だけ見たいよ〜という方はこちらをクリックしてください↓

<details><summary>Javascriptコード全体</summary>

```application.js
document.addEventListener('turbolinks:load', () => {

  // 新規登録バリデーション
  const registrationSubmitCheck = document.querySelector('#registration_submit');
  if (registrationSubmitCheck) {

    let nameError = false;
    let emailError = false;
    let passError = false;
    let passConfirmError = false;

    const registrationName = document.querySelector('#registration_name');
    const registrationEmail = document.querySelector('#registration_email');
    const registrationPass = document.querySelector('#registration_pass');
    const registrationPassConfirm = document.querySelector('#registration_pass_confirm');
    const registrationSubmit = document.querySelector('#registration_submit');

    const inputs = [registrationName, registrationEmail, registrationPass, registrationPassConfirm];

    inputs.forEach(input => {
      input.addEventListener('input', () => {
        updateSubmit();
      });
    });

    // ユーザー名のinputイベントを監視
    registrationName.addEventListener('input', function() {
      let name = this.value;
      if (name.length < 1) {
        nameError = true;
        document.querySelector('#nameError').textContent = 'ユーザー名を入力してください。';
        document.querySelector('#nameError').style.display = 'block';
        updateSubmit();
      } else {
        fetch(`/users/check_name?name=${name}`)
          .then(response => response.json())
          .then(data => {
            if (data.status === false) {
              nameError = true;
              document.querySelector('#nameError').textContent = 'このユーザー名は既に登録されています。';
              document.querySelector('#nameError').style.display = 'block';
            } else {
              nameError = false;
              document.querySelector('#nameError').style.display = 'none';
            }
            updateSubmit();
          })
          .catch(error => {
            console.log("AJAXリクエスト失敗:", error);
          });
      }
    });

    registrationEmail.addEventListener('input', function() {
      let email = this.value;
      let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        emailError = true;
        document.querySelector('#emailError').textContent = 'メールアドレスの形式が正しくありません。';
        document.querySelector('#emailError').style.display = 'block';
        updateSubmit();
      } else {
        fetch(`/users/check_email?email=${email}`)
          .then(response => response.json())
          .then(data => {
            if (data.status === false) {
              emailError = true;
              document.querySelector('#emailError').textContent = 'このメールアドレスは既に登録されています。';
              document.querySelector('#emailError').style.display = 'block';
            } else {
              emailError = false;
              document.querySelector('#emailError').style.display = 'none';
            }
            updateSubmit();
          })
          .catch(error => {
            console.log("AJAXリクエスト失敗:", error);
          });
      }
    });

    // パスワードフィールドのinputイベントを監視
    registrationPass.addEventListener('input', function() {
      if (this.value.length < 6) {
        passError = true;
        updatePasswordError();
        document.querySelector('#passError').style.display = 'block';
      } else {
        passError = false;
        document.querySelector('#passError').style.display = 'none';
      }
      updateSubmit();
    });

    // パスワード最低文字数カウント
    function updatePasswordError() {
      let remaining = 6 - registrationPass.value.length;
      document.querySelector('#remainingChars').textContent = remaining;
    }

    // パスワード確認フィールドのinputイベントを監視
    registrationPassConfirm.addEventListener('input', function() {
      if (this.value !== registrationPass.value) {
        passConfirmError = true;
        document.querySelector('#passConfirmError').style.display = 'block';
      } else {
        passConfirmError = false;
        document.querySelector('#passConfirmError').style.display = 'none';
      }
      updateSubmit();
    });

    function updateSubmit() {
      if (registrationName.value.trim() === '' ||
          registrationEmail.value.trim() === '' ||
          registrationPass.value.trim() === '' ||
          registrationPassConfirm.value.trim() === '' ||
          nameError || emailError || passError || passConfirmError) {
        registrationSubmit.classList.add('disabled');
        registrationSubmit.setAttribute('disabled', true);
      } else {
        registrationSubmit.classList.remove('disabled');
        registrationSubmit.removeAttribute('disabled');
      }
    }
  }

  // ログインバリデーション
  const loginSubmitCheck = document.querySelector('#login_submit');
  if (loginSubmitCheck) {

    let loginEmailError = false;
    let loginPassError = false;
    let loginError = false;

    document.querySelectorAll('#login_email, #login_pass').forEach(element => {
      element.addEventListener('input', function() {
        if (document.querySelector('#login_email').value.trim() === '' || document.querySelector('#login_pass').value.trim() === '') {
          document.querySelector('#login_submit').classList.add('disabled');
          document.querySelector('#login_submit').setAttribute('disabled', true);
        } else if (loginEmailError || loginPassError || loginError) {
          document.querySelector('#login_submit').classList.add('disabled');
          document.querySelector('#login_submit').setAttribute('disabled', true);
        } else {
          document.querySelector('#login_submit').classList.remove('disabled');
          document.querySelector('#login_submit').removeAttribute('disabled');
        }
      });
    });

    // メールアドレスのinputイベントを監視
    document.querySelector('#login_email').addEventListener('input', function() {
      let email = this.value;
      let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        document.querySelector('#loginEmailError').textContent = 'メールアドレスの形式が正しくありません。';
        document.querySelector('#loginEmailError').style.display = 'block';
        loginEmailError = true;
      } else {
        document.querySelector('#loginEmailError').style.display = 'none';
        loginEmailError = false;
      }
      updateLoginSubmit();
    });

    // パスワードフィールドのinputイベントを監視
    document.querySelector('#login_pass').addEventListener('input', function() {
      if (this.value.length < 6) {
        updateLoginPasswordError();
        document.querySelector('#loginPassError').style.display = 'block';
        loginPassError = true;
      } else {
        document.querySelector('#loginPassError').style.display = 'none';
        loginPassError = false;
      }
      updateLoginSubmit();
    });

    // パスワード最低文字数カウント
    function updateLoginPasswordError() {
      let remaining = 6 - document.querySelector('#login_pass').value.length;
      document.querySelector('#loginRemainingChars').textContent = remaining;
    }

    function updateLoginSubmit() {
      if (document.querySelector('#login_email').value.trim() === '' || document.querySelector('#login_pass').value.trim() === '' || loginEmailError || loginPassError) {
        document.querySelector('#login_submit').classList.add('disabled');
        document.querySelector('#login_submit').setAttribute('disabled', true);
      } else {
        document.querySelector('#login_submit').classList.remove('disabled');
        document.querySelector('#login_submit').removeAttribute('disabled');
      }
    }

    document.addEventListener("ajax:success", function(e) {
      let data = e.detail[0];
      if (data.status === true) {
        location.href = "/";
      } else if (data.status === false) {
        document.querySelector('#loginError').textContent = 'メールアドレスまたはパスワードが間違っています。';
        document.querySelector('#loginError').style.display = 'block';
      } else if (data.status === "inactive") {
        document.querySelector('#loginError').textContent = '退会済みです。別のメールアドレスをお使いください。';
        document.querySelector('#loginError').style.display = 'block';
      }
    });

    document.addEventListener("ajax:error", function(e) {
      document.querySelector('#loginError').textContent = '通信エラーが発生しました。';
      document.querySelector('#loginError').style.display = 'block';
    });

  }
});
```
</details>

#### 新規登録バリデーション
完成図
[![Image from Gyazo](https://i.gyazo.com/69e29e3c3abc88b6e333d1d8eb2c9497.gif)](https://gyazo.com/69e29e3c3abc88b6e333d1d8eb2c9497)



<details><summary>新規登録バリデーション Javascriptコード全体</summary>

```application.js
  // 新規登録バリデーション
  const registrationSubmitCheck = document.querySelector('#registration_submit');
  if (registrationSubmitCheck) {

    let nameError = false;
    let emailError = false;
    let passError = false;
    let passConfirmError = false;

    const registrationName = document.querySelector('#registration_name');
    const registrationEmail = document.querySelector('#registration_email');
    const registrationPass = document.querySelector('#registration_pass');
    const registrationPassConfirm = document.querySelector('#registration_pass_confirm');
    const registrationSubmit = document.querySelector('#registration_submit');

    const inputs = [registrationName, registrationEmail, registrationPass, registrationPassConfirm];

    inputs.forEach(input => {
      input.addEventListener('input', () => {
        updateSubmit();
      });
    });

    // ユーザー名のinputイベントを監視
    registrationName.addEventListener('input', function() {
      let name = this.value;
      if (name.length < 1) {
        nameError = true;
        document.querySelector('#nameError').textContent = 'ユーザー名を入力してください。';
        document.querySelector('#nameError').style.display = 'block';
        updateSubmit();
      } else {
        fetch(`/users/check_name?name=${name}`)
          .then(response => response.json())
          .then(data => {
            if (data.status === false) {
              nameError = true;
              document.querySelector('#nameError').textContent = 'このユーザー名は既に登録されています。';
              document.querySelector('#nameError').style.display = 'block';
            } else {
              nameError = false;
              document.querySelector('#nameError').style.display = 'none';
            }
            updateSubmit();
          })
          .catch(error => {
            console.log("AJAXリクエスト失敗:", error);
          });
      }
    });

    registrationEmail.addEventListener('input', function() {
      let email = this.value;
      let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        emailError = true;
        document.querySelector('#emailError').textContent = 'メールアドレスの形式が正しくありません。';
        document.querySelector('#emailError').style.display = 'block';
        updateSubmit();
      } else {
        fetch(`/users/check_email?email=${email}`)
          .then(response => response.json())
          .then(data => {
            if (data.status === false) {
              emailError = true;
              document.querySelector('#emailError').textContent = 'このメールアドレスは既に登録されています。';
              document.querySelector('#emailError').style.display = 'block';
            } else {
              emailError = false;
              document.querySelector('#emailError').style.display = 'none';
            }
            updateSubmit();
          })
          .catch(error => {
            console.log("AJAXリクエスト失敗:", error);
          });
      }
    });

    // パスワードフィールドのinputイベントを監視
    registrationPass.addEventListener('input', function() {
      if (this.value.length < 6) {
        passError = true;
        updatePasswordError();
        document.querySelector('#passError').style.display = 'block';
      } else {
        passError = false;
        document.querySelector('#passError').style.display = 'none';
      }
      updateSubmit();
    });

    // パスワード最低文字数カウント
    function updatePasswordError() {
      let remaining = 6 - registrationPass.value.length;
      document.querySelector('#remainingChars').textContent = remaining;
    }

    // パスワード確認フィールドのinputイベントを監視
    registrationPassConfirm.addEventListener('input', function() {
      if (this.value !== registrationPass.value) {
        passConfirmError = true;
        document.querySelector('#passConfirmError').style.display = 'block';
      } else {
        passConfirmError = false;
        document.querySelector('#passConfirmError').style.display = 'none';
      }
      updateSubmit();
    });

    function updateSubmit() {
      if (registrationName.value.trim() === '' ||
          registrationEmail.value.trim() === '' ||
          registrationPass.value.trim() === '' ||
          registrationPassConfirm.value.trim() === '' ||
          nameError || emailError || passError || passConfirmError) {
        registrationSubmit.classList.add('disabled');
        registrationSubmit.setAttribute('disabled', true);
      } else {
        registrationSubmit.classList.remove('disabled');
        registrationSubmit.removeAttribute('disabled');
      }
    }
  }
```
</details>

ブロックに分けて記載していきます

##### 登録ボタンについて
まず前提として、新規登録フォームのsubmitボタンはデフォルトで表示された時クリックできないように実装しています

```public/registarations/_form.html.erb
<%= f.submit "登録", disabled: true, class: "disabled", id: "registration_submit" %>
```

```application.css
.disabled {
  cursor: not-allowed;
}
```

HTMLの機能で`disabled: true`と記述するだけです！
自動でボタンの色がグレーっぽくなります
`class: "disabled"`でcssの記述によりホバーした時にカーソルがバツになります

[![Image from Gyazo](https://i.gyazo.com/49f94ec6ff3c4e25c7e2dc182f03a4de.gif)](https://gyazo.com/49f94ec6ff3c4e25c7e2dc182f03a4de)

```application.js
  const registrationSubmitCheck = document.querySelector('#registration_submit');
  if (registrationSubmitCheck) {

    let nameError = false;
    let emailError = false;
    let passError = false;
    let passConfirmError = false;

    const registrationName = document.querySelector('#registration_name');
    const registrationEmail = document.querySelector('#registration_email');
    const registrationPass = document.querySelector('#registration_pass');
    const registrationPassConfirm = document.querySelector('#registration_pass_confirm');
    const registrationSubmit = document.querySelector('#registration_submit');

    const inputs = [registrationName, registrationEmail, registrationPass, registrationPassConfirm];

    inputs.forEach(input => {
      input.addEventListener('input', () => {
        updateSubmit();
      });
    });
  
:

    function updateSubmit() {
      if (registrationName.value.trim() === '' ||
          registrationEmail.value.trim() === '' ||
          registrationPass.value.trim() === '' ||
          registrationPassConfirm.value.trim() === '' ||
          nameError || emailError || passError || passConfirmError) {
        registrationSubmit.classList.add('disabled');
        registrationSubmit.setAttribute('disabled', true);
      } else {
        registrationSubmit.classList.remove('disabled');
        registrationSubmit.removeAttribute('disabled');
      }
    }
```

`let nameError = false;`などは、エラー状態かを表します
`false`だとエラーでない状態、`true`だとエラー状態を意味します
初期値はエラーでない`false`です

```application.js
    const registrationName = document.querySelector('#registration_name');
    const registrationEmail = document.querySelector('#registration_email');
    const registrationPass = document.querySelector('#registration_pass');
    const registrationPassConfirm = document.querySelector('#registration_pass_confirm');
    const registrationSubmit = document.querySelector('#registration_submit');

    const inputs = [registrationName, registrationEmail, registrationPass, registrationPassConfirm];

    inputs.forEach(input => {
      input.addEventListener('input', () => {
        updateSubmit();
      });
    });
```

このコードは４つの入力フィールドのinputイベント(入力イベント)を監視し、
フィールドに何かしらの入力があった場合は`updateSubmit`関数が呼び出されます

```application.js
    function updateSubmit() {
      if (registrationName.value.trim() === '' ||
          registrationEmail.value.trim() === '' ||
          registrationPass.value.trim() === '' ||
          registrationPassConfirm.value.trim() === '' ||
          nameError || emailError || passError || passConfirmError) {
        registrationSubmit.classList.add('disabled');
        registrationSubmit.setAttribute('disabled', true);
      } else {
        registrationSubmit.classList.remove('disabled');
        registrationSubmit.removeAttribute('disabled');
      }
    }
```

`updateSubmit`関数では以下のことが行われています
・`.value.trim() === ''`で4つのフィールドが空白でないか確認します
　（`.trim()`は、前後の空白を取り除く）
・エラーフラグを確認します
　(`nameError`など4つ全てがエラー状態でないことを確認)
・エラーが一つでもある場合登録ボタンはクリックできないまま(一つでもtrueがある場合)
・エラーが一つもない場合登録ボタンをクリックできるようにする（全てfalseだった場合）
　(classのdisabledを削除して、disabled属性をfalseに変える↓)

```diff_ruby
- <%= f.submit "登録", disabled: true, class: "disabled", id: "registration_submit" %>
+ <%= f.submit "登録", disabled: false, class: "", id: "registration_submit" %>
```

また、Javascriptにおいて`==`と`===`の違いは以下の記事がわかりやすかったです

https://qiita.com/PianoScoreJP/items/e43d70ec188c6fed73ed


##### ユーザー名のバリデーション(ajax使うよ)
```application.js
    // ユーザー名のinputイベントを監視
    registrationName.addEventListener('input', function() {
      let name = this.value;
      if (name.length < 1) {
        nameError = true;
        document.querySelector('#nameError').textContent = 'ユーザー名を入力してください。';
        document.querySelector('#nameError').style.display = 'block';
        updateSubmit();
      } else {
        fetch(`/users/check_name?name=${name}`)
          .then(response => response.json())
          .then(data => {
            if (data.status === false) {
              nameError = true;
              document.querySelector('#nameError').textContent = 'このユーザー名は既に登録されています。';
              document.querySelector('#nameError').style.display = 'block';
            } else {
              nameError = false;
              document.querySelector('#nameError').style.display = 'none';
            }
            updateSubmit();
          })
          .catch(error => {
            console.log("AJAXリクエスト失敗:", error);
          });
      }
    });
```

```application.js
    registrationName.addEventListener('input', function() {
      let name = this.value;
      if (name.length < 1) {
        nameError = true;
        document.querySelector('#nameError').textContent = 'ユーザー名を入力してください。';
        document.querySelector('#nameError').style.display = 'block';
        updateSubmit();
```

ユーザー名の入力フィールドのinputイベント(入力イベント)を監視し、
何かしら入力された後に、deleteなどで文字数が1未満になった時、
「ユーザー名を入力してください。」というエラー文が`nameError`のidが付与されている箇所に表示されます↓

```public/registrations/_form.html.erb
<%= f.text_field :name, id: "registration_name"%>
<div id="nameError""></div>
```

次に、条件分岐で1文字以上入力されている時の処理です
```application.js
      } else {
        fetch(`/users/check_name?name=${name}`)
          .then(response => response.json())
          .then(data => {
```

ajaxでurlから欲しいデータを取得します
そのためにはルーティングとuserコントローラにアクションを記述する必要があります
今回はpublicとadminにルーティングを分けていますのでご留意ください

```routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'public/registrations',
    sessions: 'public/sessions',
  }
  :
  scope module: :public do
  :
    get 'users/check_name' => 'users#check_name'
  end
  :
end
```

```users_controllre.rb
class Public::UsersController < ApplicationController
  before_action :authenticate_user!, except: [:check_name]
:
  def check_name
    name = params[:name]
    user = User.find_by(name: name)
    render json: user.nil? ? {status: true}.to_json : {status: false}.to_json
  end
:
end
```

フォームに入力されたユーザー名を`fine_by`で検索
ユーザーがnilの場合は`{status: true}`をjson形式返し、入力されたユーザー名と同一のユーザー名がデータベースに保存されている場合は、`{status: false}`をjson形式で返します

```application.js
            if (data.status === false) {
              nameError = true;
              document.querySelector('#nameError').textContent = 'このユーザー名は既に登録されています。';
              document.querySelector('#nameError').style.display = 'block';
            } else {
              nameError = false;
              document.querySelector('#nameError').style.display = 'none';
            }
            updateSubmit();
          })
          .catch(error => {
            console.log("AJAXリクエスト失敗:", error);
          });
```
もしajaxで取得したデータが `{status: false}`の場合
・`nameError`を`true`にします（エラー状態を意味します)
・「このユーザー名は既に登録されています。」というエラー文が`nameError`のidが付与されている箇所に表示されます↓

```public/registrations/_form.html.erb
<%= f.text_field :name, id: "registration_name"%>
<div id="nameError""></div>
```

もしajaxで取得したデータが `{status: true}`の場合
・`nameError`のidが付与されている箇所が非表示になります

もしajaxで取得したデータが `{status: error}`の場合
・AJAXリクエスト失敗とコンソールに表示されます


##### メールアドレスのバリデーション
最初の部分以外はユーザー名と同じなので省略します

```application.js
    registrationEmail.addEventListener('input', function() {
      let email = this.value;
      let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        emailError = true;
        document.querySelector('#emailError').textContent = 'メールアドレスの形式が正しくありません。';
        document.querySelector('#emailError').style.display = 'block';
        updateSubmit();
```

ここで正規表現を利用して、「⚪︎⚪︎＠⚪︎⚪︎.⚪︎⚪︎」の形式であることをチェックしています

##### パスワードのバリデーション
```application.js
    // パスワードフィールドのinputイベントを監視
    registrationPass.addEventListener('input', function() {
      if (this.value.length < 6) {
        passError = true;
        updatePasswordError();
        document.querySelector('#passError').style.display = 'block';
      } else {
        passError = false;
        document.querySelector('#passError').style.display = 'none';
      }
      updateSubmit();
    });

    // パスワード最低文字数カウント
    function updatePasswordError() {
      let remaining = 6 - registrationPass.value.length;
      document.querySelector('#remainingChars').textContent = remaining;
    }
```

入力されたパスワードが6文字以上かチェックしています
6文字未満の場合`passError`のidが付与されているエラー文が表示され、
6から入力された文字数を引いた数を`remainingChars`のidが付与されている箇所に挿入します↓

```public/registrations/_form.html.erb
<%= f.password_field :password, autocomplete: "new-password", id: "registration_pass" %>
<div id="passError">パスワードを入力してください（あと<span id="remainingChars"></span>文字）。</div>
```

##### パスワード確認のバリデーション
```application.js
    registrationPassConfirm.addEventListener('input', function() {
      if (this.value !== registrationPass.value) {
        passConfirmError = true;
        document.querySelector('#passConfirmError').style.display = 'block';
      } else {
        passConfirmError = false;
        document.querySelector('#passConfirmError').style.display = 'none';
      }
      updateSubmit();
    });
```

入力されたパスワード確認をパスワードと一致するかチェックし、一致しなければ
`passConfirmError`のidが付与されているエラー文が表示されます↓

```public/registrations/_form.html.erb
<%= f.password_field :password_confirmation, autocomplete: "new-password", id: "registration_pass_confirm" %>
<div id="passConfirmError">パスワードが一致していません。</div>
```

#### ログインバリデーション
完成図
[![Image from Gyazo](https://i.gyazo.com/c9f6a88b5a16d994ed52164bd518d04c.gif)](https://gyazo.com/c9f6a88b5a16d994ed52164bd518d04c)

<details><summary>ログインバリデーション Javascriptコード全体</summary>

```application.js
  // ログインバリデーション
  const loginSubmitCheck = document.querySelector('#login_submit');
  if (loginSubmitCheck) {

    let loginEmailError = false;
    let loginPassError = false;
    let loginError = false;

    document.querySelectorAll('#login_email, #login_pass').forEach(element => {
      element.addEventListener('input', function() {
        if (document.querySelector('#login_email').value.trim() === '' || document.querySelector('#login_pass').value.trim() === '') {
          document.querySelector('#login_submit').classList.add('disabled');
          document.querySelector('#login_submit').setAttribute('disabled', true);
        } else if (loginEmailError || loginPassError || loginError) {
          document.querySelector('#login_submit').classList.add('disabled');
          document.querySelector('#login_submit').setAttribute('disabled', true);
        } else {
          document.querySelector('#login_submit').classList.remove('disabled');
          document.querySelector('#login_submit').removeAttribute('disabled');
        }
      });
    });

    // メールアドレスのinputイベントを監視
    document.querySelector('#login_email').addEventListener('input', function() {
      let email = this.value;
      let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailRegex.test(email)) {
        document.querySelector('#loginEmailError').textContent = 'メールアドレスの形式が正しくありません。';
        document.querySelector('#loginEmailError').style.display = 'block';
        loginEmailError = true;
      } else {
        document.querySelector('#loginEmailError').style.display = 'none';
        loginEmailError = false;
      }
      updateLoginSubmit();
    });

    // パスワードフィールドのinputイベントを監視
    document.querySelector('#login_pass').addEventListener('input', function() {
      if (this.value.length < 6) {
        updateLoginPasswordError();
        document.querySelector('#loginPassError').style.display = 'block';
        loginPassError = true;
      } else {
        document.querySelector('#loginPassError').style.display = 'none';
        loginPassError = false;
      }
      updateLoginSubmit();
    });

    // パスワード最低文字数カウント
    function updateLoginPasswordError() {
      let remaining = 6 - document.querySelector('#login_pass').value.length;
      document.querySelector('#loginRemainingChars').textContent = remaining;
    }

    function updateLoginSubmit() {
      if (document.querySelector('#login_email').value.trim() === '' || document.querySelector('#login_pass').value.trim() === '' || loginEmailError || loginPassError) {
        document.querySelector('#login_submit').classList.add('disabled');
        document.querySelector('#login_submit').setAttribute('disabled', true);
      } else {
        document.querySelector('#login_submit').classList.remove('disabled');
        document.querySelector('#login_submit').removeAttribute('disabled');
      }
    }

    document.addEventListener("ajax:success", function(e){
      let data = e.detail[0];
      if (data.status === true) {
        location.href = "/";
      } else {
        document.querySelector('#loginError').textContent = 'メールアドレスまたはパスワードが間違っています。';
        document.querySelector('#loginError').style.display = 'block';
      }
    });

    document.addEventListener("ajax:error", function(e) {
      document.querySelector('#loginError').textContent = '通信エラーが発生しました。';
      document.querySelector('#loginError').style.display = 'block';
    });

  }
```

</details>

ユーザー名とメールアドレスについては新規登録と同じなので省略します

##### ログインフォームURLについて
まず前提として、ログインフォームのURLはajaxのデータ送信に使用します
理由はログインボタンをクリックして入力情報が誤っていたときにモーダル上でエラー文を表示できないためです（モーダルでない元のログインページにリダイレクトしてしまいます）

```public/sessions/_form.html.erb
<%= form_with model: resource, url: users_check_user_path, local: false, class: 'sessions' do |f| %>
```

```routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'public/registrations',
    sessions: 'public/sessions',
  }
  :
  scope module: :public do
  :
    post 'users/check_user' => 'users#check_user'
  end
  :
end
```
今回ルーティングがgetでなくpostなのはパスワードがURLに表示されてしまうことを防ぐためです

```users_controllre.rb
class Public::UsersController < ApplicationController
  before_action :authenticate_user!, except: [:check_user]
:
  def check_user
    user = User.find_by(email: params[:user][:email])
    if !user.nil? && user.valid_password?(params[:user][:password])
      if user.is_active?
        sign_in user
        flash[:notice] = "ログインしました。"
        render json: {status: true}.to_json
      else
        render json: {status: "inactive"}.to_json
      end
    else
      render json: {status: false}.to_json
    end
  end
:
end
```
ajaxによって送信されたemailを他のユーザーがすでに登録しているかfine_byで確認します
一致していた場合は`sign_in user`でログインし、`{status: true}`を返します
一致しなかった場合は、`{status: false}`を返します

##### ログインエラー

```application.js
    document.addEventListener("ajax:success", function(e) {
      let data = e.detail[0];
      if (data.status === true) {
        location.href = "/";
      } else if (data.status === false) {
        document.querySelector('#loginError').textContent = 'メールアドレスまたはパスワードが間違っています。';
        document.querySelector('#loginError').style.display = 'block';
      } else if (data.status === "inactive") {
        document.querySelector('#loginError').textContent = '退会済みです。別のメールアドレスをお使いください。';
        document.querySelector('#loginError').style.display = 'block';
      }
    });
```

`{status: true}`がtrueであればトップページ(URLが/)に飛び、
`{status: false}`であれば`loginError'`のidが付与されている箇所に「メールアドレスまたはパスワードが間違っています。」というエラー文が表示されます↓

```public/sessions/_form.html.erb
<div id="loginError"></div>
```

# さいごに
最初はjQueryで書いてましたが、最近はjQueryを使わず素のJavascriptで書くことが多くなってきたらしいです！ということで素のJavascriptに書き換えました（ChatGPTでですが💦）
修正点等ございましたらご教示お願いいたします！

https://qiita.com/3rarara/items/6450603f2ea29f5d7793