symfony/stimulus-bundle
Symfony bundle that integrates Stimulus with Symfony and Symfony UX. Adds Twig stimulus_* helpers for controllers/actions/targets, supports AssetMapper, and provides a service to build Stimulus data attributes for use in templates and services.
Install the Bundle:
composer require symfony/stimulus-bundle
Ensure your config/bundles.php includes:
return [
// ...
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
];
Enable AssetMapper (if not already configured):
composer require symfony/asset-mapper-bundle
Update config/packages/asset_mapper.yaml to include Stimulus controllers:
framework:
asset_mapper:
excluded_patterns: ['*/controllers.json']
First Stimulus Controller:
Create a controller in assets/controllers/ (e.g., hello_controller.js):
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.element.textContent = 'Hello, Stimulus!';
}
}
Use in Twig:
<div {{ stimulus_controller('hello') }}></div>
This renders as:
<div data-controller="hello"></div>
Create a controller (assets/controllers/validation_controller.js):
export default class extends Controller {
static targets = ['field'];
validate() {
const value = this.fieldTarget.value;
if (!value) {
this.fieldTarget.setCustomValidity('This field is required');
} else {
this.fieldTarget.setCustomValidity('');
}
}
}
Use in a form (templates/form.html.twig):
<input
type="text"
{{ stimulus_controller('validation') }}
{{ stimulus_target('validation.field') }}
{{ stimulus_action('validation#validate', { trigger: 'blur' }) }}
>
Controllers: Declare Stimulus controllers in Twig:
{{ stimulus_controller('modal', { id: 'user-modal' }) }}
Renders: <div data-controller="modal" data-modal-id-value="user-modal">.
Actions: Trigger actions with parameters:
{{ stimulus_action('modal#open', { userId: 42 }) }}
Renders: <button data-action="modal#open" data-modal-open-user-id-value="42">.
Targets: Reference DOM elements:
{{ stimulus_target('modal.backdrop') }}
Renders: <div data-modal-target="backdrop">.
Combined Example:
<div {{ stimulus_controller('modal', { id: 'user-modal' }) }}>
<button {{ stimulus_action('modal#open', { userId: 42 }) }}>
Open Modal
</button>
<div {{ stimulus_target('modal.backdrop') }}></div>
</div>
Use the StimulusHelper service to generate data attributes dynamically:
// src/Controller/ExampleController.php
public function show(StimulusHelper $stimulusHelper): Response
{
$data = $stimulusHelper->controller('modal', ['id' => 'user-modal']);
return $this->render('modal.html.twig', ['stimulus_data' => $data]);
}
In Twig:
<div {{ stimulus_data(stimulus_data) }}></div>
assets/controllers/ is scanned:
# config/packages/asset_mapper.yaml
framework:
asset_mapper:
packages:
stimulus:
globs: ['assets/controllers/**/*.{js,ts}']
// webpack.config.js
Encore
.addEntry('stimulus', './assets/controllers/index.js')
.splitEntry('stimulus');
// assets/controllers/hello_controller.ts
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() { /* ... */ }
}
package.json includes:
{
"type": "module",
"scripts": {
"build": "tsc && npm run build:js"
}
}
{{ stimulus_controller('turbo-modal') }}
<turbo-frame id="modal-frame">
{{ stimulus_target('turbo-modal.content') }}
</turbo-frame>
// assets/controllers/updates_controller.js
export default class extends Controller {
connect() {
this.subscription = this.element.Mercure.subscribe('/updates', (event) => {
this.element.textContent = event.data;
});
}
}
Controller Organization:
assets/controllers/forms/, assets/controllers/modals/).assets/controllers/index.js) to export all controllers:
export { default as HelloController } from './hello_controller';
export { default as ValidationController } from './validation_controller';
Twig Template Structure:
{% include 'components/modal.html.twig' with {
'controller': 'modal',
'id': 'user-modal'
} %}
Debugging:
data-controller and data-action attributes in browser dev tools to inspect Stimulus events.log(event) {
console.log('Action triggered:', event.detail);
}
Testing:
DomCrawler:
public function testModalController(): void
{
$crawler = $this->client->request('GET', '/modal');
$controller = $crawler->filterXPath('//div[@data-controller="modal"]')->data('controller');
$this->assertEquals('modal', $controller);
}
AssetMapper Exclusions:
excluded_patterns in asset_mapper.yaml is misconfigured.excluded_patterns does not include */controllers.json (v2.33+):
framework:
asset_mapper:
excluded_patterns: ['*/node_modules/**', '*/var/**']
Case Sensitivity in Parameters:
stimulus_action are normalized to camelCase (v2.13.0 BC break).
{{ stimulus_action('example#action', { bigCrocodile: 'value' }) }}
Old: event.params.bigcrocodile (incorrect).
New: event.params.bigCrocodile (correct).action(event) {
console.log(event.detail.bigCrocodile); // Correct
}
Windows Path Issues:
TypeScript Module System:
type: "module" in package.json may cause issues with older Node.js versions or misconfigured bundlers.type: "commonjs" if needed (v2.13.2 reverted this change).Stimulus Outlets:
{{ stimulus_target('modal--backdrop') }} <!-- Correct -->
{{ stimulus_target('modal.backdrop') }} <!-- Incorrect -->
Symfony UX Package Discovery:
assets/ is the root directory for UX packages or configure custom paths in config/packages/stimulus.yaml:
stimulus:
packages:
- '@symfony/ux-stimulus'
- './assets/packages'
How can I help you explore Laravel packages today?