1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Stimulusで作る「フォームを初期値に戻す」リセット機能

1
Last updated at Posted at 2026-01-16

はじめに

管理画面でデータを編集中、「やっぱり元に戻したい」と思うことはありませんか?

ブラウザの「フォームリセット」機能は、HTMLの初期値(value属性)に戻すものであり、サーバーから取得した値に戻すわけではありません。本記事では、ページ表示時の値(サーバーから取得した値)に戻すStimulusコントローラーを、段階的に実装していきます。

完成イメージ

  • 「リセット」ボタンをクリック → 確認ダイアログ表示
  • 「OK」を押すと、該当項目がページ表示時の値に戻る
  • グループ単位でリセット対象を絞り込める

stimulus-form-reset-controller.png

Stimulusコントローラーとは

Stimulusは、HTMLにdata-*属性を付けるだけでJavaScriptの処理を紐づけられる軽量フレームワークです。Rails 7以降のデフォルトとして採用されています。

基本的な使い方は以下の3つです:

<div data-controller="form-reset">
  <%# ↑ form_reset_controller.js が自動的に接続される %>

  <button data-action="click->form-reset#reset">リセット</button>
  <%# ↑ クリック時に reset() メソッドを呼び出す %>

  <input data-form-reset-target="input">
  <%# ↑ this.inputTargets でこの要素を参照できる %>
</div>
  • data-controller: コントローラー名を指定(ファイル名の_controller.jsを除いた部分)
  • data-action: イベント名->コントローラー名#メソッド名 でイベントハンドラを設定
  • data-xxx-target: コントローラー内からthis.xxxTargetで参照可能な要素を定義

詳細は公式ハンドブックを参照してください。

なぜこの記事でStimulusを使うのか

リセット機能は「ボタンをクリック → 対象要素の値を初期値に戻す」という明確なアクションです。Stimulusを使うと:

  • アクションの宣言: data-action="click->form-reset#reset"で、ボタンと処理の紐づけがHTMLで完結
  • ターゲットの指定: data-form-reset-target="input"で、リセット対象の要素を明示的に選択できる
  • グループ化が容易: 後述するように、data-form-reset-group-valueで対象を絞り込む拡張も自然に実装できる

素のJavaScriptでも実装できますが、Stimulusを使うことで「どのボタンが何をリセットするか」がHTMLを見るだけで把握でき、保守性が向上します。

Step 1: 最小限の実装

まずは、最もシンプルな実装から始めましょう。

要件

  • リセットボタンを押すと、フォームの値がページ表示時の値に戻る

どうやって実現するか

「元に戻す」ためには、比較対象となる初期値をあらかじめ保存しておく必要があります。

初期値の保存方法:

  • JavaScriptのMapオブジェクトを使用
  • キー: DOM要素自体(input要素)
  • 値: その要素のvalue
  • Stimulusのconnect()(コントローラーがDOMに接続されたタイミング)で保存

リセット対象の指定方法:

  • Stimulus Targetsを使用(data-form-reset-target="input"
  • これにより「リセット対象にしたい要素」を明示的に指定できる
  • 全ての要素を対象にするのではなく、必要な要素だけを登録

リセットの実行:

  • 保存した初期値を各要素のvalueに再代入

実装

// app/javascript/controllers/form_reset_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["input"];

  connect() {
    this.initialValues = new Map();
    this.inputTargets.forEach((input) => {
      this.initialValues.set(input, input.value);
    });
  }

  reset() {
    this.inputTargets.forEach((input) => {
      input.value = this.initialValues.get(input);
    });
  }
}
<div data-controller="form-reset">
  <input type="text" name="name" value="初期値" data-form-reset-target="input">
  <button type="button" data-action="form-reset#reset">リセット</button>
</div>

成果

  • リセットボタンを押すと、テキストフィールドがページ表示時の値に戻る

問題点

  1. checkbox/radioが動かない - valueではなくcheckedを見る必要がある
  2. 確認なしでリセットされてしまう - 誤操作で編集内容が消える危険
  3. 全項目が一括でリセットされる - 「この項目だけ戻したい」ができない

Step 2: checkbox/radio対応と確認ダイアログ

要件

  • checkbox/radioもリセットできるようにする
  • 誤操作防止のため、リセット前に確認ダイアログを表示する

どうやって実現するか

checkbox/radioの値の取り方:

  • checkbox/radioはvalue属性ではなくchecked属性(boolean)で状態を管理している
  • 初期値保存時: input.checkedを保存
  • 復元時: input.checkedに代入

確認ダイアログ:

  • window.confirm()を使用(ブラウザ標準のダイアログ)
  • 「キャンセル」が押されたらreturnで処理を中断

実装

以降のStepでは、前述と同じ部分を // ... で省略します。完成版のコードは最後のStep 5に掲載しています。

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["input"];

  connect() {
    this.initialValues = new Map();
    this.inputTargets.forEach((input) => {
      this.saveInitialValue(input); // 変更: メソッド化
    });
  }

  // 追加: checkbox/radioは checked を保存
  saveInitialValue(input) {
    if (input.type === "checkbox" || input.type === "radio") {
      this.initialValues.set(input, input.checked);
    } else {
      this.initialValues.set(input, input.value);
    }
  }

  reset(event) {
    event.preventDefault(); // 追加

    // 追加: 確認ダイアログ
    if (!confirm("初期状態に戻しますか?")) {
      return;
    }

    this.inputTargets.forEach((input) => {
      this.restoreInitialValue(input); // 変更: メソッド化
    });
  }

  // 追加: checkbox/radioは checked を復元
  restoreInitialValue(input) {
    const initialValue = this.initialValues.get(input);
    if (initialValue === undefined) return;

    if (input.type === "checkbox" || input.type === "radio") {
      input.checked = initialValue;
    } else {
      input.value = initialValue;
    }
  }
}

成果

  • checkbox/radioもリセットできる
  • 確認ダイアログが表示され、誤操作を防げる

問題点

  1. 確認ダイアログのメッセージが固定 - 「商品情報を初期状態に戻しますか?」のようにカスタマイズしたい
  2. 全項目が一括でリセットされる - 「住所だけ戻したい」ができない

Step 3: Stimulus Paramsで確認メッセージをカスタマイズ

要件

  • ボタンごとに異なる確認メッセージを表示できるようにする

どうやって実現するか

Stimulus Paramsの仕組み:

  • ボタン要素にdata-{controller}-{param-name}-param属性を付ける
  • アクションメソッド内でevent.params.paramNameとして取得できる
  • これにより、同じアクションでもボタンごとに異なるパラメータを渡せる

パラメータ名の命名規則:

  • HTML属性: data-form-reset-target-name-param(ケバブケース)
  • JavaScript: event.params.targetName(キャメルケース)
  • Stimulusが自動的に変換してくれる

実装

export default class extends Controller {
  static targets = ["input"];

  // connect(), saveInitialValue(), restoreInitialValue() は前述と同様

  reset(event) {
    event.preventDefault();

    // 追加: Stimulus Paramsでメッセージをカスタマイズ
    const targetName = event.params.targetName || "この項目";

    if (!confirm(`「${targetName}」を初期状態に戻しますか?`)) {
      return;
    }

    this.inputTargets.forEach((input) => {
      this.restoreInitialValue(input);
    });
  }
}
<div data-controller="form-reset">
  <input type="text" name="name" data-form-reset-target="input">
  <button type="button"
          data-action="form-reset#reset"
          data-form-reset-target-name-param="商品名">
    <%# ↑ 追加: パラメータでメッセージをカスタマイズ %>
    リセット
  </button>
</div>

これで「「商品名」を初期状態に戻しますか?」と表示されます。

成果

  • ボタンごとにメッセージをカスタマイズできる
  • ユーザーにとって分かりやすい確認ダイアログになった

問題点

  • まだ全項目が一括でリセットされる - グループ単位でリセットしたい

Step 4: グループ単位でのリセット

要件

  • 「基本情報」だけ、「詳細情報」だけ、といった部分リセットを可能にする

どうやって実現するか

グループの指定方法:

  • 各フォーム要素にdata-form-reset-group属性を付ける
  • 例: data-form-reset-group="basic"
  • リセットボタンにもdata-form-reset-group-paramでグループ名を指定

フィルタリングのロジック:

  1. ボタンからグループ名を取得(event.params.group
  2. 各要素のグループ名と比較(input.dataset.formResetGroup
  3. グループが一致する要素のみリセット
  4. グループ指定がないボタンは全要素をリセット(従来の動作)

実装

export default class extends Controller {
  static targets = ["input"];

  // connect(), saveInitialValue(), restoreInitialValue() は前述と同様

  reset(event) {
    event.preventDefault();

    const group = event.params.group; // 追加: グループ名を取得
    const targetName = event.params.targetName || "この項目";

    if (!confirm(`「${targetName}」を初期状態に戻しますか?`)) {
      return;
    }

    this.inputTargets.forEach((input) => {
      // 追加: グループが指定されている場合、該当グループのみリセット
      if (group && input.dataset.formResetGroup !== group) {
        return; // グループが違えばスキップ
      }
      this.restoreInitialValue(input);
    });
  }
}
<div data-controller="form-reset">
  <!-- 基本情報グループ -->
  <fieldset>
    <legend>基本情報</legend>
    <input type="text" name="name"
           data-form-reset-target="input"
           data-form-reset-group="basic">
    <input type="number" name="price"
           data-form-reset-target="input"
           data-form-reset-group="basic">
    <button type="button"
            data-action="form-reset#reset"
            data-form-reset-group-param="basic"
            data-form-reset-target-name-param="基本情報">
      基本情報をリセット
    </button>
  </fieldset>

  <!-- 詳細情報グループ -->
  <fieldset>
    <legend>詳細情報</legend>
    <textarea name="description"
              data-form-reset-target="input"
              data-form-reset-group="detail"></textarea>
    <button type="button"
            data-action="form-reset#reset"
            data-form-reset-group-param="detail"
            data-form-reset-target-name-param="詳細情報">
      詳細情報をリセット
    </button>
  </fieldset>

  <!-- 全体リセット(グループ指定なし) -->
  <button type="button"
          data-action="form-reset#reset"
          data-form-reset-target-name-param="全ての項目">
    全てリセット
  </button>
</div>

成果

  • 「基本情報」だけ、「詳細情報」だけ、といった部分リセットができる
  • グループ指定なしのボタンで全体リセットも可能

問題点

  • 他のコントローラーがリセットを検知できない - 例えば「変更検知機能」と連携したい

Step 5: カスタムイベントで他コントローラーと連携(完成版)

要件

  • リセット実行を他のコントローラーに通知する
  • 例: 変更検知機能(form-dirty)がリセット後に背景色を更新できるようにする

どうやって実現するか

changeイベントの発火:

  • 値を復元しただけでは、ブラウザはイベントを発火しない
  • input.dispatchEvent(new Event("change", { bubbles: true }))で手動発火
  • これにより、変更検知機能(form-dirty)が再評価される

カスタムイベントの発火:

  • リセット完了後にform-reset:completedイベントを発火
  • CustomEventを使用し、detailにグループ名などの情報を含める
  • bubbles: trueでイベントをバブリングさせ、親要素でもキャッチ可能に

カスタムイベントを使う理由:

  • 各要素のchangeイベントだけでは「リセットが完了した」というタイミングを知れない
  • まとめて何か処理をしたい場合に有用

完成版の実装

// app/javascript/controllers/form_reset_controller.js
import { Controller } from "@hotwired/stimulus";

// フォームを初期状態(ページ表示時の値)に戻すコントローラー
// グループ属性でフィルタリングしてリセット対象を絞り込める
export default class extends Controller {
  static targets = ["input"];

  connect() {
    this.initialValues = new Map();
    this.inputTargets.forEach((input) => {
      this.saveInitialValue(input);
    });
  }

  saveInitialValue(input) {
    if (input.type === "checkbox" || input.type === "radio") {
      this.initialValues.set(input, input.checked);
    } else {
      this.initialValues.set(input, input.value);
    }
  }

  reset(event) {
    event.preventDefault();

    const group = event.params.group;
    const targetName = event.params.targetName || "この項目";

    if (!confirm(`「${targetName}」を初期状態に戻しますか?`)) {
      return;
    }

    this.inputTargets.forEach((input) => {
      if (group && input.dataset.formResetGroup !== group) {
        return;
      }
      this.restoreInitialValue(input);
    });

    // リセット完了後にカスタムイベントを発火(他コントローラー連携用)
    this.element.dispatchEvent(
      new CustomEvent("form-reset:completed", {
        detail: { group },
        bubbles: true,
      }),
    );
  }

  restoreInitialValue(input) {
    const initialValue = this.initialValues.get(input);
    if (initialValue === undefined) return;

    if (input.type === "checkbox" || input.type === "radio") {
      input.checked = initialValue;
    } else {
      input.value = initialValue;
    }

    // changeイベントを発火して他のコントローラーに通知
    input.dispatchEvent(new Event("change", { bubbles: true }));
  }
}

changeイベント発火の効果

// 値を復元した後、changeイベントを発火
input.dispatchEvent(new Event("change", { bubbles: true }));

これにより:

  • 変更検知機能(form-dirty)が再評価される → リセット後に背景色が元に戻る
  • フォームバリデーションがトリガーされる
  • 依存関係のある他のフィールドが更新される

カスタムイベントの活用例

// 別のコントローラーや別の箇所でリセット完了を検知
document.addEventListener("form-reset:completed", (event) => {
  console.log(`グループ ${event.detail.group} がリセットされました`);
  // 追加の処理...
});

最終的な成果

  • ページ表示時の値にリセット(HTMLの初期値ではない)
  • checkbox/radioも正しくリセット
  • 確認ダイアログ付き(メッセージカスタマイズ可能)
  • グループ単位でのリセット
  • 他コントローラーとの連携(changeイベント、カスタムイベント)

関連機能:変更検知機能との関係

本記事の「リセット」機能と、「変更を視覚的に表示する」dirty機能は、どちらも初期値をMapで保持するという共通点があります。

しかし、実装時の検討の結果、これらは独立したコントローラーとして分離しました。

なぜ分離したのか

選択肢 メリット デメリット
1つのコントローラーに統合 初期値のMapが1つで済む reset機能だけ使いたいページでもdirty機能のコードが読み込まれる
独立したコントローラーに分離 単機能で使いやすい、必要な機能だけ導入できる 両方使う場合、初期値のMapが2つ存在する

分離を選んだ理由:

  1. 使用頻度の違い: dirty機能は全ページで使いたいが、reset機能は特定の編集画面だけで使う
  2. メモリコストは軽微: 初期値のMapが2つ存在しても、要素数×数十バイト程度
  3. 依存関係の複雑さ回避: 初期値管理を共通化すると、コントローラー間の初期化順序を気にする必要が出る
  4. 単一責任の原則: 各コントローラーが1つの責務に集中できる

併用時の動作

両方のコントローラーを同じ画面で使用しても問題ありません。

<body data-controller="form-dirty">
  <div data-controller="form-reset">
    <input type="text"
           data-form-reset-target="input"
           name="product[name]">
    <button data-action="form-reset#reset">リセット</button>
  </div>
</body>

リセット実行時には各入力要素でchangeイベントが発火するため、dirty機能も自動的に再評価されます。つまり、リセット後は背景色が元に戻ります。

変更検知機能の詳細な実装については、関連記事『Stimulus + MutationObserver + Turboで作る「フォーム変更の視覚的フィードバック」』をご覧ください。

まとめ

本記事では、Stimulusを使ってフォームを初期値に戻すリセット機能を段階的に実装しました。

Step 内容 解決した問題
1 最小限の実装 基本的なリセット
2 checkbox/radio対応、確認ダイアログ 異なるフォーム要素タイプ、誤操作防止
3 Stimulus Params 確認メッセージのカスタマイズ
4 グループ単位リセット 部分的なリセット
5 イベント発火 他コントローラーとの連携

この実装は汎用的で、どのRailsプロジェクトにも導入できます。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?