
Scene 1: 問題発見!文字数が更新されない 😱

プロ太先生、大変です!面談記録の文字数カウントが全然更新されないんです…

どれどれ、見せてみたまえ。ああ、これはよくある問題だね。JavaScript で`getElementById`を使って実装しているが、Rails の`form_with`で生成されるフォーム要素との相性が悪いんだ。

React なら useState を使えばすぐに解決できるのに…Rails だと難しいんですか?

いや、実は Rails には**Stimulus**という素晴らしいツールがあるんだ!React の useState のような動的な更新を、Rails らしい方法で実現できるよ。
Scene 2: Stimulus って何?🤔

Stimulus…初めて聞きました!

Stimulus は Rails 標準の JavaScript フレームワークだよ。**HTML-first**の思想で、既存の HTML に JavaScript の機能を後から追加できるんだ。
Stimulus の特徴
**HTML 中心**: HTML に`data-*`属性を追加するだけ
**軽量**: React/Vue のような大きなライブラリは不要
**Rails 標準**: Turbo・Hotwire と連携して動作
**段階的導入**: 既存のプロジェクトに簡単導入

なるほど!でも具体的にはどう使うんですか?
Scene 3: 実際のコードで学ぼう!💻

OZ君の文字数カウンター問題を例に、実際のコードを見てみよう。
1.Stimulus コントローラーの作成

まず、`app/javascript/controllers/character_counter_controller.js`を作成するよ!
// Stimulusの基本クラスをインポート
import { Controller } from "@hotwired/stimulus";
// Stimulusコントローラーを定義(クラス名は自由だが、ファイル名と対応させる)
export default class extends Controller {
// HTMLのどの要素をターゲットにするか定義
// "input"と"counter"という名前のターゲットを設定
static targets = ["input", "counter"];
// HTMLから渡される値を定義
// 最小文字数(min)と最大文字数(max)を数値として受け取る
static values = { min: Number, max: Number };
// コントローラーがDOMに接続された時に自動実行
// Reactの useEffect(() => {}, []) に相当
connect() {
this.updateCount(); // 初期表示時に文字数を更新
}
// 文字数カウントを更新するメソッド
// Reactの setState() に相当する処理
updateCount() {
// inputTargetから現在のテキストを取得
const text = this.inputTarget.value;
const count = text.length;
// counterTargetに文字数を表示
// ReactのuseStateで状態を更新するのと同じ効果
this.counterTarget.textContent = count;
// 文字数に応じてスタイルを動的に変更
// 条件によってCSSクラスを切り替え
if (count < this.minValue) {
this.counterTarget.className = "text-red-600"; // 文字数不足:赤色
} else if (count > this.maxValue) {
this.counterTarget.className = "text-red-600"; // 文字数超過:赤色
} else {
this.counterTarget.className = "text-green-600"; // 適切な文字数:緑色
}
}
}

おお!これがコントローラーですか。React のコンポーネントみたいですね!

その通り!そして次は、HTML でこのコントローラーを使うんだ。
2. HTML ビューでの使用方法

以下が、
`app/views/auth/interview_record.html.erb`の該当部分だよ。
<!-- data-controller属性でStimulusコントローラーを指定 -->
<!-- data-*-value属性で値をJavaScriptに渡す -->
<div data-controller="character-counter"
data-character-counter-min-value="10"
data-character-counter-max-value="2000">
<label class="block text-sm font-medium text-gray-700 mb-2">
面談内容 <span class="text-red-500">*</span>
</label>
<!-- textareaをinputターゲットとして指定 -->
<!-- data-action属性でイベントとメソッドを紐付け -->
<%= form.text_area :content,
rows: 12,
class: "w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none",
data: {
character_counter_target: "input", # inputターゲットとして指定
action: "input->character-counter#updateCount" # inputイベント時にupdateCountメソッド実行
} %>
<p class="mt-2 text-sm text-gray-500">
10文字以上2000文字以内で入力してください。
<span class="ml-2">
<!-- counterターゲットとして指定:ここに文字数が表示される -->
現在: <span data-character-counter-target="counter">0</span>文字
</span>
</p>
</div>

なるほど!HTML の`data-*`属性で JavaScript と連携するんですね!

そうです。では、具体的にどう連携するのか、step-by-step で詳しく見てみよう。
Step 1: `data-controller` でコントローラーを接続
<!-- ① この div要素にStimulusコントローラーを接続 -->
<div data-controller="character-counter">
<!-- この要素の中で character-counter が有効になる -->
</div>
何が起こるかの説明
– `character-counter` → `CharacterCounterController` クラスを自動で探す
– ファイル名: `character_counter_controller.js` → クラスと自動マッピング
– この div の中でコントローラーの `connect()` メソッドが実行される
Step 2: `data-*-target` で要素を指定
<div data-controller="character-counter">
<!-- ② inputターゲットとして textarea を指定 -->
<textarea data-character-counter-target="input"></textarea>
<!-- ③ counterターゲットとして span を指定 -->
<span data-character-counter-target="counter">0</span>
</div>
何が起こるかの説明
// JavaScript側で、こうアクセスできるようになる
this.inputTarget; // ← textarea要素を取得
this.counterTarget; // ← span要素を取得
// つまり、getElementById() の代わりに、こんな感じ:
// Before: document.getElementById('my-textarea')
// After: this.inputTarget
Step 3: `data-action` でイベントを紐付け
<div data-controller="character-counter">
<!-- ④ inputイベントが発生したらupdateCountメソッドを実行 -->
<textarea
data-character-counter-target="input"
data-action="input->character-counter#updateCount"
></textarea>
<span data-character-counter-target="counter">0</span>
</div>
何が起こるかの説明
– `input` = イベント名(文字入力のたび)
– `character-counter` = コントローラー名
– `updateCount` = 実行するメソッド名
– つまり: **「文字入力されたら updateCount メソッドを呼び出して」**
Step 4: `data-*-*-value` で値を渡す
<div
data-controller="character-counter"
data-character-counter-min-value="10"
data-character-counter-max-value="2000"
>
<textarea
data-character-counter-target="input"
data-action="input->character-counter#updateCount"
></textarea>
<span data-character-counter-target="counter">0</span>
</div>
何が起こるかの説明
// JavaScript側で、こう使える
this.minValue // ← 10 が入る(Number型に自動変換)
this.maxValue // ← 2000 が入る(Number型に自動変換)
// values定義があるから自動で型変換される
static values = { min: Number, max: Number }
実際の動作フロー

つまり、どういう順番で動くんですか?

実際の動作を順番に見てみよう。
1️⃣ ページ読み込み
↓
2️⃣ data-controller=”character-counter” を発見
↓
3️⃣ CharacterCounterController の connect() が実行
↓
4️⃣ this.updateCount() で初期の文字数表示
↓
5️⃣ ユーザーがテキストエリアに文字入力
↓
6️⃣ inputイベント発生
↓
7️⃣ data-action で指定された updateCountメソッド実行
↓
8️⃣ this.inputTarget.value で現在の文字を取得
↓
9️⃣ this.counterTarget.textContent で文字数を表示更新
Stimulus の魔法のポイント

なるほど!`data-*`属性だけで、こんなに連携できるんですね!

そうなんだ!ポイントは以下の 4 つだよ。
1. **自動マッピング**: `data-controller=”character-counter”` → `CharacterCounterController`
2. **自動要素取得**: `data-*-target=”input”` → `this.inputTarget`
3. **自動イベント設定**: `data-action=”input->*#updateCount”` → 自動でイベントリスナー設定
4. **自動型変換**: `data-*-*-value=”10″` → `this.minValue` (Number 型)
**再利用性**の高さ

この`character-counter`って、他の場所でも使えるんですか?

もちろん!OZ君も既に気づいているはずだよ。モーダルでも同じコントローラーを使っているだろう?
<!-- 👍 同じコントローラーを別の場所で再利用 -->
<div data-controller="character-counter" data-character-counter-min-value="5" data-character-counter-max-value="500">
<!-- コメント用のフォーム -->
</div>
<div data-controller="character-counter" data-character-counter-min-value="10" data-character-counter-max-value="2000">
<!-- 面談記録用のフォーム -->
</div>
Scene 4: 実践!他の使用例

理解を深めるために、他の例も見ておこう。
例1:自動保存機能
// auto_save_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["form", "status"];
static values = { url: String, interval: Number };
connect() {
this.autoSave = setInterval(() => {
this.save();
}, this.intervalValue);
}
save() {
// フォームデータを自動保存
this.statusTarget.textContent = "保存中...";
// Ajax処理...
}
}
例 2: 検索の候補表示
// search_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["input", "results"];
search() {
const query = this.inputTarget.value;
if (query.length > 2) {
// 検索API呼び出し
this.showResults(results);
}
}
}

なるほど!いろんな場面で使えるんですね!
まとめ
Stimulus のポイント
1. **HTML-first**: HTML が主役、JavaScript は脇役
2. **軽量**: 必要最小限の機能で高速
3. **Rails 標準**: Turbo/Hotwire との連携が抜群
4. **再利用性**: 一度作れば色々な場所で使える
5. **学習コスト**: 基本的な JavaScript の知識があれば十分
いつ使うべき?
– ✅ **部分的な動的機能**が欲しい時
– ✅ **既存の Rails アプリ**に機能追加したい時
– ✅ **軽量な解決策**が欲しい時
– ✅ **Rails Way**に沿って開発したい時

プロ太先生、ありがとうございました!Stimulus が Rails での動的 UI 更新の強い味方だということがよく分かりました!

そうだね。React の useState のような機能を Rails らしい方法で実現できるのが Stimulus の魅力だ。これからの Rails 開発では必須のツールになるよ!