| name | stimulus-controller |
| description | Use when adding or modifying a Stimulus controller in `frontend/src/controllers/`. Covers naming, registration, targets/values/actions conventions, custom events, and the matching test template. |
Skill: Stimulus Controller
Naming
- File:
snake_case_controller.ts (e.g., tag_palette_controller.ts).
- Class:
PascalCase ending in Controller. Default export.
- Stimulus identifier: derived from filename:
tag_palette_controller.ts
→ tag-palette → data-controller="tag-palette".
Register in frontend/src/main.ts:
import { Application } from "@hotwired/stimulus";
import TagPaletteController from "./controllers/tag_palette_controller";
const app = Application.start();
app.register("tag-palette", TagPaletteController);
Skeleton
import { Controller } from "@hotwired/stimulus";
export default class extends Controller<HTMLElement> {
static targets = ["chip", "name"];
static values = { activeType: String };
static classes = ["dragging"];
declare readonly chipTargets: HTMLElement[];
declare readonly nameTarget: HTMLInputElement;
declare activeTypeValue: string;
declare readonly draggingClass: string;
connect() {
}
disconnect() {
}
pick(event: MouseEvent) {
this.dispatch("picked", { detail: { type: "text" } });
}
}
Conventions
- Always
declare typed targets/values to keep strict happy.
- Prefer custom events (
this.dispatch(name, { detail })) over
cross-controller imports. Other controllers listen via
data-action="tag-palette:picked@window->scrapper#onTagPicked".
- Cleanup in
disconnect() — remove window listeners, clear timers.
- No direct Tailwind class manipulation for state. Use
static classes
data-state="..." attributes for assertion-friendly DOM.
- No DOM queries outside
this.element / targets. If you need it,
add a target.
Public surface (what tests assert)
For each controller, define and document:
- Inputs: targets, values, parent attributes.
- Outputs: events dispatched (with detail shape), DOM mutations,
data-state changes.
- Failure modes: invalid input → emits no event, sets error attribute.
Tests live next to the controller and exercise items 1–3. See the
testing skill for templates.
Anti-patterns
- Storing state in DOM via free-form
data-* you also read elsewhere
without a target. Always go through Stimulus targets/values.
- Using
document.querySelector to reach across controllers — use
events.
- Network calls inline. Always go through
lib/api.ts so tests can
mock at one place.
- Mutating the recipe state from multiple controllers. Recipe state
lives only in
scrapper_controller; others request changes via
events.
Checklist before marking done