StimulusBundle: Symfony integration with Stimulus
Tip
Check out live demos of Symfony UX at https://ux.symfony.com!
This bundle adds integration between Symfony, Stimulus and the Symfony UX packages:
- Twig
stimulus_functions & filters to add Stimulus controllers, actions & targets in your templates; - Integration to load UX Packages (extra Stimulus controllers)
Installation
First, if you don't have one yet, choose and install an asset handling system; they all work great with StimulusBundle:
- AssetMapper: PHP-based system for handling assets
- Webpack Encore: Node-based packaging system built on Webpack
- Reprise: Node-based integration for Vite and Rsbuild (experimental)
See Reprise vs Encore vs AssetMapper to learn which is best for your project.
Next, install the bundle:
1
$ composer require symfony/stimulus-bundle
If you're using Symfony Flex, you're done! The recipe will update the necessary files. If not, or you're curious, see Manual Setup.
Tip
If you're using Encore, be sure to install your assets (e.g. npm install)
and restart Encore.
Usage
You can now create custom Stimulus controllers inside of the assets/controllers
directory. In fact, you should have an example controller there already: hello_controller.js:
1 2 3 4 5 6 7
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}
Then, activate the controller in your HTML:
1 2 3
<div data-controller="hello">
...
</div>
Optionally, this bundle has a Twig function to render the attribute:
1 2 3 4 5 6 7 8
<div {{ stimulus_controller('hello') }}>
...
</div>
<!-- would render -->
<div data-controller="hello">
...
</div>
That's it! Whenever this element appears on the page, the hello controller
will activate.
There's a lot more to learn about Stimulus. See the Stimulus Documentation for all the goodies.
TypeScript Controllers
If you want to use TypeScript to define your controllers, you can! Install and set up the
sensiolabs/typescript-bundle. Then be sure to add the assets/controllers path to the
sensiolabs_typescript.source_dir configuration. Finally, create your controller in that
directory and you're good to go.
The UX Packages
Symfony provides a set of UX packages that add extra Stimulus controllers to solve
common problems. StimulusBundle activates any 3rd party Stimulus controllers
that are mentioned in your assets/controllers.json file. This file is updated
whenever you install a UX package.
Check out the official UX packages.
Lazy Stimulus Controllers
By default, all of your controllers (i.e. files in assets/controllers/ +
controllers in assets/controllers.json) will be downloaded and loaded on
every page.
Sometimes you may have a controller that's only used on some pages. In that case,
you can make the controller "lazy". In this case, will not be downloaded on
initial page load. Instead, as soon as an element appears on the page matching
the controller (e.g. <div data-controller="hello">), the controller - and anything
else it imports - will be lazily-loaded via Ajax.
To make one of your custom controllers lazy, add a special comment on top:
1 2 3 4 5 6
import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
// ...
}
To make a third-party controller lazy, in assets/controllers.json, set
fetch to lazy.
Note
If you write your controllers using TypeScript and you're using
StimulusBundle 2.21.0 or earlier, make sure removeComments is not set
to true in your TypeScript config.
Stimulus Tools around the World
Because Stimulus is used by developers outside of Symfony, many tools exist beyond the UX packages:
- stimulus-use: Add composable behaviors to your Stimulus controllers, like debouncing, detecting outside clicks and many other things.
- stimulus-components A large number of pre-made Stimulus controllers, like for Copying to clipboard, Sortable, Popover (similar to tooltips) and much more.
Stimulus Twig Helpers
This bundle adds some Twig functions/filters to help add Stimulus controllers, actions and targets in your templates.
Note
Though this bundle provides these helpful Twig functions/filters, it's recommended to use raw data attributes instead, as they're straightforward.
Tip
If you use PhpStorm IDE - you may want to install Stimulus plugin to get nice auto-completion for the attributes.
stimulus_controller
This bundle ships with a special stimulus_controller() Twig function
that can be used to render Stimulus Controllers & Values and CSS Classes.
Stimulus Controllers can also reference other controllers by using Outlets.
For example:
1 2 3 4 5 6 7 8 9 10 11 12
<div {{ stimulus_controller('hello', { 'name': 'World', 'data': [1, 2, 3, 4] }) }}>
Hello
</div>
<!-- would render -->
<div
data-controller="hello"
data-hello-name-value="World"
data-hello-data-value="[1,2,3,4]"
>
Hello
</div>
If you want to set CSS classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<div {{ stimulus_controller('hello', { 'name': 'World', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }) }}>
Hello
</div>
<!-- would render -->
<div
data-controller="hello"
data-hello-name-value="World"
data-hello-data-value="[1,2,3,4]"
data-hello-loading-class="spinner"
>
Hello
</div>
<!-- or without values -->
<div {{ stimulus_controller('hello', controllerClasses: { 'loading': 'spinner' }) }}>
Hello
</div>
And with outlets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<div {{ stimulus_controller('hello',
{ 'name': 'World', 'data': [1, 2, 3, 4] },
{ 'loading': 'spinner' },
{ 'other': '.target' } ) }}>
Hello
</div>
<!-- would render -->
<div
data-controller="hello"
data-hello-name-value="World"
data-hello-data-value="[1,2,3,4]"
data-hello-loading-class="spinner"
data-hello-other-outlet=".target"
>
Hello
</div>
<!-- or without values/classes -->
<div {{ stimulus_controller('hello', controllerOutlets: { 'other': '.target' }) }}>
Hello
</div>
Any non-scalar values (like data: [1, 2, 3, 4]) are JSON-encoded. And all
values are properly escaped (the string [ is an escaped
[ character, so the attribute is really [1,2,3,4]).
If you have multiple controllers on the same element, you can chain them as
there's also a stimulus_controller filter:
1 2 3 4 5 6 7 8
<div {{ stimulus_controller('hello', { 'name': 'World' })|stimulus_controller('other-controller') }}>
Hello
</div>
<!-- would render -->
<div data-controller="hello other-controller" data-hello-name-value="World">
Hello
</div>
You can also retrieve the generated attributes as an array, which can be helpful e.g. for forms:
1
{{ form_start(form, { attr: stimulus_controller('hello', { 'name': 'World' }).toArray() }) }}
stimulus_action
The stimulus_action() Twig function can be used to render Stimulus Actions.
For example:
1 2 3 4 5 6
<div {{ stimulus_action('controller', 'method') }}>Hello</div>
<div {{ stimulus_action('controller', 'method', 'click') }}>Hello</div>
<!-- would render -->
<div data-action="controller#method">Hello</div>
<div data-action="click->controller#method">Hello</div>
If you have multiple actions and/or methods on the same element, you can chain
them as there's also a stimulus_action filter:
1 2 3 4 5 6 7 8
<div {{ stimulus_action('controller', 'method')|stimulus_action('other-controller', 'test') }}>
Hello
</div>
<!-- would render -->
<div data-action="controller#method other-controller#test">
Hello
</div>
You can also retrieve the generated attributes as an array, which can be helpful e.g. for forms:
1
{{ form_row(form.password, { attr: stimulus_action('hello-controller', 'checkPasswordStrength').toArray() }) }}
You can also pass parameters to actions:
1 2 3 4
<div {{ stimulus_action('hello-controller', 'method', 'click', { 'count': 3 }) }}>Hello</div>
<!-- would render -->
<div data-action="click->hello-controller#method" data-hello-controller-count-param="3">Hello</div>
stimulus_target
The stimulus_target() Twig function can be used to render Stimulus Targets.
For example:
1 2 3 4 5 6
<div {{ stimulus_target('controller', 'myTarget') }}>Hello</div>
<div {{ stimulus_target('controller', 'myTarget secondTarget') }}>Hello</div>
<!-- would render -->
<div data-controller-target="myTarget">Hello</div>
<div data-controller-target="myTarget secondTarget">Hello</div>
If you have multiple targets on the same element, you can chain them as there's
also a stimulus_target filter:
1 2 3 4 5 6 7 8
<div {{ stimulus_target('controller', 'myTarget')|stimulus_target('other-controller', 'anotherTarget') }}>
Hello
</div>
<!-- would render -->
<div data-controller-target="myTarget" data-other-controller-target="anotherTarget">
Hello
</div>
You can also retrieve the generated attributes as an array, which can be helpful e.g. for forms:
1
{{ form_row(form.password, { attr: stimulus_target('hello-controller', 'myTarget').toArray() }) }}
Chaining Different Helpers
The stimulus_controller, stimulus_action and stimulus_target
filters can be mixed freely to render all the attributes of an element at
once, whichever function the chain started with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<div {{ stimulus_controller('first-controller')
|stimulus_target('second-controller', 'anotherTarget')
|stimulus_target('third-controller', 'foo')
|stimulus_action('first-controller', 'test')
|stimulus_controller('fourth-controller')
|stimulus_action('fourth-controller', 'onClick') }}
>
Hello
</div>
<!-- would render -->
<div data-controller="first-controller fourth-controller"
data-action="first-controller#test fourth-controller#onClick"
data-second-controller-target="anotherTarget"
data-third-controller-target="foo"
>
Hello
</div>
Stimulus Attributes from PHP
The same attributes are available from PHP through the StimulusHelper
service, which you can autowire. Reach for it when the element you want to
decorate is not written in a template, a form field for instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// src/Form/EventType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
class EventType extends AbstractType
{
public function __construct(private StimulusHelper $stimulusHelper)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$attributes = $this->stimulusHelper->createStimulusAttributes();
$attributes->addController('country-picker', ['locale' => 'fr']);
$attributes->addTarget('country-picker', 'select');
$attributes->addAction('country-picker', 'refresh', 'change');
$builder->add('country', CountryType::class, [
'attr' => $attributes->toArray(),
]);
}
}
The field then renders with the attributes the Twig helpers would have produced:
1 2 3 4 5 6
<select
data-controller="country-picker"
data-action="change->country-picker#refresh"
data-country-picker-target="select"
data-country-picker-locale-value="fr"
>
Cast the object to a string when you need the rendered attributes rather than
an array, and use addAttribute() to carry along an attribute that is not a
Stimulus one.
Configuration
If you're using AssetMapper, you can configure the path to your controllers
directory and the controllers.json file if you need to use different paths:
1 2 3 4 5 6
# config/packages/stimulus.yaml
stimulus:
# the default values
controller_paths:
- '%kernel.project_dir%/assets/controllers'
controllers_json: '%kernel.project_dir%/assets/controllers.json'
Manual Installation Details
When you install this bundle, its Flex recipe should handle updating all the files needed. If you're not using Flex or want to double-check the changes, check out the StimulusBundle Flex recipe. Here's a summary of what's inside:
assets/stimulus_bootstrap.jsstarts the Stimulus application and loads your controllers. It's imported byassets/app.jsand its exact content depends on whether you have Webpack Encore or AssetMapper installed (see below).assets/app.jsis updated to importassets/stimulus_bootstrap.jsassets/controllers.jsonThis file starts (mostly) empty and is automatically updated as your install UX packages that provide Stimulus controllers.assets/controllers/This directory is where you should put your custom Stimulus controllers. It comes with one examplehello_controller.jsfile.
A few other changes depend on which asset system you're using:
With AssetMapper
If you're using AssetMapper, two new entries will be added to your importmap.php
file:
1 2 3 4 5 6 7 8 9 10 11
// importmap.php
return [
// ...
'@symfony/stimulus-bundle' => [
'path' => '@symfony/stimulus-bundle/loader.js',
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
];
The recipe will update your assets/stimulus_bootstrap.js file to look like this:
1 2 3 4
// assets/stimulus_bootstrap.js
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
The @symfony/stimulus-bundle refers the one of the new entries in your
importmap.php file. This file is dynamically built by the bundle and
will import all your custom controllers as well as those from controllers.json.
It will also dynamically enable "debug" mode in Stimulus when your application
is running in debug mode.
With WebpackEncoreBundle
If you're using Webpack Encore, the recipe will also update your webpack.config.js
file to include this line:
1 2
// webpack.config.js
.enableStimulusBridge('./assets/controllers.json')
The assets/stimulus_bootstrap.js file will be updated to look like this:
1 2 3 4 5 6 7 8 9
// assets/stimulus_bootstrap.js
import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.[jt]sx?$/
));
The @hotwired/stimulus package will be added to your package.json file.
The Webpack Encore integration also relies on @symfony/stimulus-bridge, which is specific to Encore,
so install it yourself:
1
$ npm install --save-dev @symfony/stimulus-bridge
With Reprise
If you're using Reprise, you must enable Stimulus by pointing the Reprise plugin at your controllers.json file:
1 2 3 4 5 6 7 8 9 10
// vite.config.js (or rsbuild.config.js)
import Symfony from '@symfony/reprise/vite';
export default defineConfig({
plugins: [
Symfony({
stimulus: './assets/controllers.json',
}),
],
});
The assets/stimulus_bootstrap.js file will be updated to look like this:
1 2 3 4
// assets/stimulus_bootstrap.js
import { startStimulusApp } from '@symfony/reprise/stimulus';
const app = startStimulusApp();
The @symfony/reprise/stimulus helper starts the application and registers all your
custom controllers along with those from controllers.json, eager or lazy, the same
way the AssetMapper loader does. The Stimulus runtime ships with the @symfony/reprise
package, so @hotwired/stimulus is the only extra package you need to install.
How are the Stimulus Controllers Loaded?
When you install a UX PHP package, Symfony Flex will automatically update your
package.json file (not done or needed if using AssetMapper) to point to a
"virtual package" that lives inside that PHP package. For example:
1 2 3 4 5 6
{
"devDependencies": {
"...": "",
"@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/assets"
}
}
This gives you a real Node package (e.g. @symfony/ux-chartjs) that, instead
of being downloaded, points directly to files that already live in your vendor/
directory.
The Flex recipe will usually also update your assets/controllers.json file
to add a new Stimulus controller to your app. For example:
1 2 3 4 5 6 7 8 9 10 11
{
"controllers": {
"@symfony/ux-chartjs": {
"chart": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}
Finally, your assets/stimulus_bootstrap.js file will automatically register:
- All files in
assets/controllers/as Stimulus controllers; - And all controllers described in
assets/controllers.jsonas Stimulus controllers.
Note
If you're using WebpackEncore, the stimulus_bootstrap.js file works in partnership
with @symfony/stimulus-bridge. With AssetMapper, the stimulus_bootstrap.js file
works directly with this bundle: a @symfony/stimulus-bundle entry is added
to your importmap.php file via Flex, which points to a file that is dynamically
built to find and load your controllers (see Configuration).
The end result: you install a package, and you instantly have a Stimulus
controller available! In this example, it's called
@symfony/ux-chartjs/chart. Well, technically, it will be called
symfony--ux-chartjs--chart. However, you can pass the original name
into the {{ stimulus_controller() }} function from WebpackEncoreBundle, and
it will normalize it:
1 2 3 4
<div {{ stimulus_controller('@symfony/ux-chartjs/chart') }}>
<!-- will render as: -->
<div data-controller="symfony--ux-chartjs--chart">
Loading Different Controllers per Part of Your App
An application can have distinct areas, for example a public site and an admin back office. By default, every registered controller loads on every page, so eagerly loaded controllers from one area ship to the other.
Note
Webpack Encore and Reprise both support this. AssetMapper does not:
startStimulusApp() in @symfony/stimulus-bundle takes no arguments
and always reads the full controllers.json.
Scoping Controllers with Reprise
startStimulusApp() from @symfony/reprise/stimulus takes no arguments,
like the AssetMapper one. What it reads is generated by the Reprise plugin,
and that generation is configurable, so each build can be given its own set
of controllers.
If a single config defines several entry points, they all share the same controller set, because there is only one plugin instance. To scope controllers to a given area of your application, give that area its own build config and point the plugin at its own controllers directory. For example, an admin area could use a dedicated config like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// vite.config.admin.ts -- run with `vite build --config vite.config.admin.ts`
import { defineConfig } from 'vite'
import Symfony from '@symfony/reprise/vite'
export default defineConfig({
input: {
admin: './assets/admin.js',
},
plugins: [
Symfony({
outputPath: 'public/admin-build',
publicPath: '/admin-build/',
stimulus: {
controllersJson: 'assets/admin/controllers.json',
controllersDir: 'assets/admin/controllers',
},
}),
],
})
This entry point then starts the application the usual way:
1 2 3 4
// assets/admin.js
import { startStimulusApp } from '@symfony/reprise/stimulus'
const app = startStimulusApp()
Register this build on the PHP side under reprise.builds so the Twig tag
functions can address it with a build argument.
See the Reprise Stimulus documentation for what else the returned application
lets you do, such as registering controllers that are not declared in
controllers.json.
Scoping Controllers with Webpack Encore
startStimulusApp() from @symfony/stimulus-bridge accepts a Webpack
context, so you can point each entry at its own controller directory. This
assets/admin.js file loads only the controllers under
controllers/admin:
1 2 3 4 5 6 7
import { startStimulusApp } from '@symfony/stimulus-bridge';
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers/admin',
true,
/\.[jt]sx?$/
));
If you keep the controllers shared between areas in their own directory, you can load several contexts into the same application:
1 2 3 4 5 6 7
import { startStimulusApp } from '@symfony/stimulus-bridge';
import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers';
const app = startStimulusApp();
app.load(definitionsFromContext(require.context('./controllers/common', true)));
app.load(definitionsFromContext(require.context('./controllers/admin', true)));
This second form drops the lazy controller loader, so add it back to the context path if you rely on lazy loading.
If you need this with AssetMapper, follow the discussion in issue #2321 in
the symfony/ux repository.