Skip to main content
stimulus-coder Use when creating or refactoring Stimulus controllers. Applies Hotwire conventions, controller design patterns, targets/values usage, action handling, and JavaScript best practices.
Aller à l'installation Skills Marketplace Découvrez et explorez les compétences IA créées par la communauté.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Copier le promptAfficher les détails du prompt Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
npx skills add https://github.com/majesticlabs-dev/majestic-marketplace --skill stimulus-coderLa commande reste sur une seule ligne. Faites défiler horizontalement pour la vérifier avant de la copier.
Vous préférez une copie locale ? Téléchargez les fichiers actuellement disponibles dans SkillsMP.
Télécharger Zip Téléchargement... Explorateur de fichiers
3 fichiers Basé sur la classification professionnelle SOC
Plus depuis ce dépôt Create distinctive, production-grade frontend interfaces with high design quality. Use when building web components, pages, or applications. Includes framework-specific guidance for Tailwind, React, Vue, and Rails/Hotwire ecosystems.
Skill file structure, naming conventions, directory layout, frontmatter requirements, and invocation control. Use when creating skill files or slash commands to ensure correct format and validation.
Forces adversarial reasoning before committing to decisions. Triggers on architectural choices, approach selection, and planning phases to prevent premature commitment bias.
name stimulus-coder description Use when creating or refactoring Stimulus controllers. Applies Hotwire conventions, controller design patterns, targets/values usage, action handling, and JavaScript best practices. allowed-tools Read Write Edit Grep Glob Bash
Stimulus Coder
Audience: Developers building interactive UIs with Stimulus.js and Hotwire.
Goal: Write maintainable Stimulus controllers where state lives in HTML and controllers add behavior.
Core Concepts
Controllers attach behavior to HTML elements
Actions respond to DOM events
Targets reference important elements
Values manage state through data attributes
Controller Design Principles
Keep Controllers Small and Reusable
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content" ]
static values = { open : Boolean }
( ) { . = ! . }
( ) {
. . . ( , ! . )
}
}
toggle
this
openValue
this
openValue
openValueChanged
this
contentTarget
classList
toggle
"hidden"
this
openValue
Use Data Attributes for Configuration export default class extends Controller {
static values = {
delay : { type : Number , default : 300 },
event : { type : String , default : "input" }
}
connect ( ) {
this .element .addEventListener (this .eventValue , this .submit .bind (this ))
}
submit ( ) {
clearTimeout (this .timeout )
this .timeout = setTimeout (() => this .element .requestSubmit (), this .delayValue )
}
}
<%= form_with data: { controller: "auto-submit", auto_submit_delay_value: 500 } %>
Compose Multiple Controllers <div data-controller="toggle clipboard" data-toggle-open-value="false">
<button data-action="toggle#toggle">Show</button>
<div data-toggle-target="content" class="hidden">
<code data-clipboard-target="source">secret-code</code>
<button data-action="clipboard#copy">Copy</button>
</div>
</div>
Targets and Values
Targets for Element References export default class extends Controller {
static targets = ["tab" , "panel" ]
static values = { index : { type : Number , default : 0 } }
select (event ) { this .indexValue = this .tabTargets .indexOf (event.currentTarget ) }
indexValueChanged ( ) {
this .panelTargets .forEach ((panel, i ) => panel.classList .toggle ("hidden" , i !== this .indexValue ))
this .tabTargets .forEach ((tab, i ) => tab.setAttribute ("aria-selected" , i === this .indexValue ))
}
}
Action Handling <button data-action="click->toggle#toggle">Toggle</button>
<input data-action="input->search#update focus->search#expand">
<button data-action="modal#open" data-modal-id-param="confirm-dialog">Open</button>
<input data-action="keydown.enter->form#submit keydown.escape->form#cancel">
Action Parameters open (event ) {
const modalId = event.params .id
document .getElementById (modalId)?.showModal ()
}
Common Controller Patterns
Dropdown Controller export default class extends Controller {
static targets = ["menu" ]
static values = { open : Boolean }
toggle ( ) { this .openValue = !this .openValue }
close (event ) {
if (!this .element .contains (event.target )) this .openValue = false
}
openValueChanged ( ) {
this .menuTarget .classList .toggle ("hidden" , !this .openValue )
if (this .openValue ) document .addEventListener ("click" , this .close .bind (this ), { once : true })
}
}
Clipboard Controller export default class extends Controller {
static targets = ["source" , "button" ]
static values = { successMessage : { type : String , default : "Copied!" } }
async copy ( ) {
const text = this .sourceTarget .value || this .sourceTarget .textContent
await navigator.clipboard .writeText (text)
this .showSuccess ()
}
showSuccess ( ) {
const original = this .buttonTarget .textContent
this .buttonTarget .textContent = this .successMessageValue
setTimeout (() => this .buttonTarget .textContent = original, 2000 )
}
}
Turbo Integration export default class extends Controller {
connect ( ) {
document .addEventListener ("turbo:before-visit" , this .dismiss .bind (this ))
this .timeout = setTimeout (() => this .dismiss (), 5000 )
}
disconnect ( ) { clearTimeout (this .timeout ) }
dismiss ( ) { this .element .remove () }
}
Architecture Patterns
Make Controllers Configurable Externalize hardcoded values into data attributes. Never embed CSS classes, selectors, or thresholds in controller logic.
export default class extends Controller {
toggle ( ) { this .element .classList .toggle ("hidden" ) }
}
export default class extends Controller {
static classes = ["toggle" ]
toggle ( ) { this .element .classList .toggle (this .toggleClass ) }
}
Mixins Over Deep Inheritance Use mixins when behavior is shared but doesn't represent specialization.
"is a" → inheritance (class extends BaseController)
"acts as" → mixin (apply behavior at connect)
"has a" → composition (separate controller + outlets)
const Sortable = (controller ) => {
const original = controller.prototype .connect
controller.prototype .connect = function ( ) {
if (original) original.call (this )
this .sortable = new Sortable (this .element , this .sortableOptions )
}
}
Targetless Controllers If a controller mixes element-level and target-level concerns, split it. Controller acting on this.element is one responsibility; acting on targets is another.
Communicate between split controllers via custom events or outlets.
Namespaced Attributes For flexible parameter sets without explicitly defining each value:
get chartOptions () {
return Object .entries (this .element .dataset )
.filter (([key] ) => key.startsWith ("chart" ))
.reduce ((opts, [key, val] ) => {
opts[key.replace ("chart" , "" ).toLowerCase ()] = val
return opts
}, {})
}
Controller Communication Choose pattern based on coupling needs:
Pattern Coupling Direction Use When Custom events Loose Broadcast (1→many) Sender doesn't know receivers Outlets Structured Direct (1→1, 1→few) Known relationships in layout Callbacks Read-only Request/response Sharing state without triggering actions
Custom Events (Preferred Default)
this .dispatch ("submitted" , { detail : { id : this .idValue }, bubbles : true })
Always set bubbles: true for cross-controller events
Namespace event names: form:submitted, cart:updated
Document the detail contract
Outlets (Structured Relationships) export default class extends Controller {
static outlets = ["result" ]
search ( ) {
const results = this .performSearch ()
this .resultOutlets .forEach (outlet => outlet.update (results))
}
resultOutletConnected (outlet ) { }
resultOutletDisconnected (outlet ) { }
}
Lifecycle Best Practices
Don't Overuse connect() connect() is for third-party plugin initialization only . Not for state setup (use Values API) or event listeners (use data-action).
connect ( ) {
this .chart = new Chart (this .canvasTarget , this .chartConfig )
}
disconnect ( ) {
this .chart .destroy ()
this .chart = null
}
Always Pair connect/disconnect Every resource acquired in connect() must be released in disconnect(). Controllers can connect/disconnect multiple times during Turbo navigation.
Turbo Cache Teardown Prevent "flash of manipulated content" when cached pages return:
connect ( ) {
document .addEventListener ("turbo:before-cache" , this .teardown .bind (this ))
this .slider = new Swiper (this .element , this .config )
}
teardown ( ) {
this .slider ?.destroy ()
}
disconnect ( ) {
this .teardown ()
}
Event Listener Hygiene
Store Bound References .bind() creates a new function each call. Store the reference for proper removal:
connect ( ) {
this .boundResize = this .resize .bind (this )
window .addEventListener ("resize" , this .boundResize , { passive : true })
}
disconnect ( ) {
window .removeEventListener ("resize" , this .boundResize )
}
Prefer Declarative Actions <%# Good: Stimulus manages lifecycle %>
<div data-controller="search"
data-action="resize@window->search#layout keydown.escape@window->search#close">
<%# Bad: manual addEventListener in connect() %>
Global events use @window or @document suffix in data-action.
Application Controller Create app/javascript/controllers/application_controller.js as a base for shared functionality:
import { Controller } from "@hotwired/stimulus"
export default class ApplicationController extends Controller {
handleError (error, context = {} ) {
console .error (`[${this .identifier} ]` , error, context)
}
}
Extend it in domain controllers:
import ApplicationController from "./application_controller"
export default class extends ApplicationController {
async save ( ) {
try {
await this .persist ()
} catch (error) {
this .handleError (error, { action : "save" , id : this .idValue })
}
}
}
Use try-catch for async operations and third-party library calls
Never swallow errors — log or report via handleError()
Use requestSubmit() not submit() for forms — fires validation and Turbo intercept
Anti-Patterns Anti-Pattern Problem Solution Creating DOM extensively Fighting Stimulus philosophy Let server render HTML Storing state in JS State lost on navigation Use Values in HTML Over-specific controllers Not reusable Design generic behaviors Manual querySelector Fragile, bypasses Stimulus Use targets Inline event handlers Unmaintainable Use data-action Overloading connect() Bloated, mixes concerns Values for state, data-action for events Tight controller coupling Fragile, hard to test Custom events or outlets Missing disconnect cleanup Memory leaks, duplicate listeners Always pair connect/disconnect Unbound event references Can't removeEventListener Store .bind() result
Output Format When creating Stimulus controllers, provide:
Controller - Complete JavaScript implementation
HTML Example - Sample markup showing usage
Configuration - Available values and targets
Integration - How it works with Turbo if applicable