All skills
smnandre avatar

/stimulus

@6d7ef8e

Stimulus JS framework for Symfony UX -- client-side behavior via HTML data attributes, zero server round-trips. Use when creating controllers for DOM manipulation, handling click/input/submit events, managing targets and values, wiring outlets between controllers, wrapping third-party JS libraries, or building toggles, dropdowns, modals, tabs, clipboard interactions. Code triggers: data-controller, data-action, data-target, data-*-value, data-*-class, data-*-outlet, stimulusFetch lazy, connect(), disconnect(), static targets, static values. Also trigger when the user asks "how do I add a click handler", "how to toggle a class", "how to build a dropdown/modal/tabs", "how to wrap a JS library in Symfony", "add keyboard shortcuts", "lazy-load a controller", "listen to global events", "communicate between controllers". Do NOT trigger for partial page updates without JS (use turbo), server-rendered reactivity (use live-component), or reusable Twig templates (use twig-component).

Use this Skill: https://skilld.dev/gh/smnandre/symfony-ux-skills/stimulus

This session only. Nothing lands on disk.

referencesapi.md

≈2.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Stimulus API Reference

Table of Contents


Lifecycle Callbacks

export default class extends Controller {
    initialize() {
        // Called once when controller is first instantiated
        // Use for one-time setup that doesn't require DOM
    }

    connect() {
        // Called each time controller connects to DOM
        // Element is available, targets are accessible
        // May be called multiple times (Turbo navigation)
    }

    disconnect() {
        // Called each time controller disconnects from DOM
        // Clean up: remove listeners, cancel timers, abort fetches
    }
}

Connection happens when:

  • Element with data-controller is in document
  • Controller identifier matches

Disconnection happens when:

  • Element removed from DOM
  • data-controller attribute removed/changed
  • Turbo replaces page body

Targets

Definition

static targets = ['query', 'results', 'errorMessage']

Generated Properties

Target name Property Returns
item this.itemTarget First element (throws if none)
item this.itemTargets Array of all elements
item this.hasItemTarget Boolean

HTML

<div data-controller="search">
    <input data-search-target="query">
    <div data-search-target="results"></div>
    <span data-search-target="errorMessage"></span>
</div>

Target Callbacks

static targets = ['item']

itemTargetConnected(element) {
    // Called when a target element is added to DOM
    // Fires before connect() on initial load
}

itemTargetDisconnected(element) {
    // Called when a target element is removed from DOM
    // Fires before disconnect()
}

Shared Targets

Element can be target for multiple controllers:

<input data-search-target="input" data-validation-target="field">

Naming

Use camelCase in JS, kebab-case in HTML:

static targets = ['errorMessage']
<span data-controller-target="errorMessage">  <!-- camelCase preserved -->

Values

Definition

static values = {
    url: String,                              // required, no default
    count: Number,
    enabled: Boolean,
    items: Array,
    config: Object,
    delay: { type: Number, default: 300 },    // with default
    query: { type: String, default: '' }
}

Types and Defaults

Type Empty default HTML encoding
String "" As-is
Number 0 Numeric string
Boolean false "true" or "false"
Array [] JSON
Object {} JSON

Generated Properties

Value name Property Description
url this.urlValue Get/set value
url this.hasUrlValue Boolean, true if attribute exists

HTML

<div data-controller="loader"
     data-loader-url-value="/api/data"
     data-loader-count-value="5"
     data-loader-enabled-value="true"
     data-loader-items-value='["a","b","c"]'
     data-loader-config-value='{"timeout":30}'>
</div>

Value Change Callbacks

static values = { count: Number }

countValueChanged(value, previousValue) {
    // Called when data-*-count-value changes
    // Also called on connect() with previousValue = undefined
    console.log(`Changed from ${previousValue} to ${value}`);
}

Setter Behavior

this.countValue = 10;        // Updates DOM attribute
this.countValue = undefined; // Removes attribute, reverts to default

Actions

Syntax

event->controller#method
event->controller#method:option

HTML Examples

<!-- Explicit event -->
<button data-action="click->dialog#open">Open</button>

<!-- Default event (click for buttons, submit for forms, input for inputs) -->
<button data-action="dialog#open">Open</button>

<!-- Multiple actions -->
<button data-action="click->a#method click->b#method">

<!-- With options -->
<form data-action="submit->form#save:prevent">
<a data-action="click->nav#go:prevent:stop">

Default Events

Element Default event
<button> click
<input type="submit"> click
<input> input
<textarea> input
<select> change
<form> submit
<details> toggle
Other click

Key Filters

<input data-action="keydown.enter->form#submit">
<input data-action="keydown.esc->form#cancel">
<input data-action="keydown.ctrl+s->form#save">
<input data-action="keydown.shift+tab->nav#previous">

Available keys: enter, tab, esc, space, up, down, left, right, home, end, page_up, page_down

Modifiers: ctrl, alt, shift, meta

Combine with +: ctrl+s, ctrl+shift+z

Global Events

<!-- Window events -->
<div data-action="resize@window->gallery#layout">
<div data-action="scroll@window->nav#highlight">

<!-- Document events -->
<div data-action="click@document->dropdown#closeOutside">
<div data-action="turbo:load@document->app#init">

Action Options

Option Effect
:prevent event.preventDefault()
:stop event.stopPropagation()
:self Only if event.target === element
:passive Passive listener (better scroll perf)
:capture Capture phase
:once Remove after first invocation

Action Parameters

Pass data via data-*-param:

<button data-action="click->item#delete"
        data-item-id-param="123"
        data-item-confirm-param="true">
    Delete
</button>
delete(event) {
    const id = event.params.id;           // "123" (string)
    const confirm = event.params.confirm; // "true" (string)
}

Event Object

handleClick(event) {
    event.target;         // Element that triggered
    event.currentTarget;  // Element with data-action
    event.params;         // Action parameters
    event.preventDefault();
    event.stopPropagation();
}

CSS Classes

Definition

static classes = ['loading', 'active', 'hidden']

Generated Properties

Class name Property Returns
loading this.loadingClass First class string
loading this.loadingClasses Array of all classes
loading this.hasLoadingClass Boolean

HTML

<div data-controller="toggle"
     data-toggle-loading-class="opacity-50 cursor-wait"
     data-toggle-active-class="bg-blue-500 text-white">
</div>

Usage

static classes = ['loading']

async submit() {
    this.element.classList.add(this.loadingClass);
    // or for multiple classes:
    this.element.classList.add(...this.loadingClasses);
    
    await fetch(...);
    
    this.element.classList.remove(this.loadingClass);
}

Outlets

Definition

static outlets = ['user-status', 'notification']

Generated Properties

Outlet name Property Returns
user-status this.userStatusOutlet First controller instance
user-status this.userStatusOutlets Array of all instances
user-status this.hasUserStatusOutlet Boolean
user-status this.userStatusOutletElement First element
user-status this.userStatusOutletElements Array of elements

HTML

<div data-controller="chat"
     data-chat-user-status-outlet=".online-user"
     data-chat-notification-outlet="#notifications">
</div>

<div class="online-user" data-controller="user-status">...</div>
<div class="online-user" data-controller="user-status">...</div>
<div id="notifications" data-controller="notification">...</div>

Outlet Callbacks

static outlets = ['user-status']

userStatusOutletConnected(outlet, element) {
    // outlet = controller instance
    // element = DOM element
}

userStatusOutletDisconnected(outlet, element) {
    // Called when outlet element removed
}

Cross-Controller Communication

// chat_controller.js
static outlets = ['user-status']

broadcastMessage(message) {
    this.userStatusOutlets.forEach(userCtrl => {
        userCtrl.showNotification(message);
    });
}

// user_status_controller.js
showNotification(message) {
    // Called from chat controller
}

Namespaced Outlets

For controllers like admin--user-status:

static outlets = ['admin--user-status']

// Access (note: no double underscore)
this.adminUserStatusOutlet
this.adminUserStatusOutlets

Controller Properties

Built-in Properties

this.element           // The controller's root element
this.application       // The Stimulus Application instance
this.identifier        // Controller name (e.g., "search")
this.scope             // Scope object with schema and element info

Data API

// Read/write data attributes on controller element
this.data.get('index')          // data-controller-index
this.data.set('index', '5')
this.data.has('index')
this.data.delete('index')

Note: Prefer Values API over Data API for typed access.

Dispatch Custom Events

// Dispatch from controller element
this.dispatch('success', { detail: { id: 123 } });

// Listen in HTML
<div data-action="search:success->results#refresh">

Options:

this.dispatch('save', {
    target: otherElement,      // default: this.element
    detail: { data: '...' },   // event.detail
    prefix: 'custom',          // event name: custom:save
    bubbles: true,             // default: true
    cancelable: true           // default: true
});

Source: SKILL.md on GitHub

No alerts15d4 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    This skill provides comprehensive documentation and implementation patterns for the Stimulus JavaScript framework. It covers lifecycle management, event handling, and common UI components using standard, secure web development practices and well-known libraries.

  • Socket15d

    No alerts

  • Snyk15d

    Risk: LOW · No issues

  • Runlayer7mo

    4 files scanned · No issues

Signed by skilld at 6d7ef8e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 4 months ago
Other metadata
metadata
{
  "author": "Simon Andre",
  "email": "smn.andre@gmail.com",
  "url": "https://smnandre.dev",
  "version": "1.2.0"
}

README badge

README badge for smnandre/symfony-ux-skills/stimulus