パスワードの変更画面で入力する確認用のパスワードをStimulusでチェック

RailsアプリでDeviseを利用してユーザー認証をしています。パスワードの変更画面ではこれまでのパスワード、新しく設定するパスワード、新しく設定するパスワード（確認）の3つのテキストフォームがあります。

確認用のパスワードが間違っていた場合に画面遷移してエラーを出力してもよいのですが、Stimulusでチェックしてみることにしました。

# Stimulusのコントローラーを作成

```sh
rails generate stimulus password_validation
```

新しく設定するパスワードとパスワード（確認）が一致しないときの挙動はいくつか考えられるのですが、今回は一致しないときにはボタンをクリックできないようにしました。

```js:app/javascript/controllers/password_validation_controller.rb
// password_validation_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["password", "confirmation", "submit"]

  connect() {
    this.validatePasswords()
  }

  validatePasswords() {
    const password = this.passwordTarget.value
    const confirmation = this.confirmationTarget.value

    if (password !== confirmation) {
      this.confirmationTarget.setCustomValidity("Passwords do not match")
    } else {
      this.confirmationTarget.setCustomValidity("")
    }

    this.submitTarget.disabled = !this.confirmationTarget.checkValidity()
  }
}
```

# パスワードの入力画面

```erb:app/views/devise/registrations/edit.html.erb
<div class="card mb-3">
  <div class="card-header">
    <%= t("views.change_password") %>
  </div>
  <div class="card-body" data-controller="password-validation">
    <%= form_for(resource, as: resource_name, url: registration_path(resource_name),
          html: { method: :put }) do |f| %>
      <%= render "devise/shared/error_messages", resource: resource %>

      <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
        <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
      <% end %>

      <div class="row g-3 mb-3">
        <div class="col-4 text-end">
          <%= f.label :current_password, class: "col-form-label" %>
        </div>
        <div class="col-8">
          <%= f.password_field :current_password, class: "form-control",
                placeholder: t("activerecord.attributes.user.current_password"),
                autocomplete: "current-password", required: true %>
        </div>
      </div>

      <div class="row g-3 mb-3">
        <div class="col-4 text-end">
          <%= f.label :password, class: "col-form-label" %>
        </div>
        <div class="col-8">
          <%= f.password_field :password, class: "form-control",
                placeholder: t("activerecord.attributes.user.password"),
                autocomplete: "new-password",  minlength: 10, required: true,
                "data-password-validation-target": "password",
                "data-action": "input->password-validation#validatePasswords" %>
        </div>
      </div>

      <div class="row g-3 mb-3">
        <div class="col-4 text-end">
          <%= f.label :password_confirmation, class: "col-form-label" %>
        </div>
        <div class="col-8">
          <%= f.password_field :password_confirmation, class: "form-control",
                placeholder: t("activerecord.attributes.user.password_confirmation"),
                autocomplete: "new-password",  minlength: 10, required: true,
                "data-password-validation-target": "confirmation",
                "data-action": "input->password-validation#validatePasswords" %>
        </div>
      </div>

      <%= f.submit class: "btn btn-primary col-4 offset-4 mt-1",
            "data-password-validation-target": "submit" %>
    <% end %>
  </div>
</div>
```

# 動作確認

新しく設定するパスワードとパスワード（確認）が一致しないとボタンをクリックできない状態になっています。

![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238166/bd327daf-8545-e81e-4c9c-6a2432e69931.png)

まだまだ迷いながらですが、Hotwire時代に適応できるよう頑張っていこうと思います。