Skip to content

StimulusBundle: Symfony integration with Stimulus

Edit this page

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; both work great with StimulusBundle:

or

See 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.

The official UX packages are:

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, 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('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }) }}>
    Hello
</div>

<!-- would render -->
<div
   data-controller="chart"
   data-chart-name-value="Likes"
   data-chart-data-value="&#x5B;1,2,3,4&#x5D;"
>
   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('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }) }}>
    Hello
</div>

<!-- would render -->
<div
   data-controller="chart"
   data-chart-name-value="Likes"
   data-chart-data-value="&#x5B;1,2,3,4&#x5D;"
   data-chart-loading-class="spinner"
>
   Hello
</div>

<!-- or without values -->
<div {{ stimulus_controller('chart', 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
<div {{ stimulus_controller('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }, { 'other': '.target' ) }}>
    Hello
</div>

<!-- would render -->
<div
   data-controller="chart"
   data-chart-name-value="Likes"
   data-chart-data-value="&#x5B;1,2,3,4&#x5D;"
   data-chart-loading-class="spinner"
   data-chart-other-outlet=".target"
>
   Hello
</div>

<!-- or without values/classes -->
<div {{ stimulus_controller('chart', 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 &#x5B; 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('chart', { 'name': 'Likes' })|stimulus_controller('other-controller') }}>
    Hello
</div>

<!-- would render -->
<div data-controller="chart other-controller" data-chart-name-value="Likes">
    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('chart', { 'name': 'Likes' }).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() }) }}

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/bootstrap.js starts the Stimulus application and loads your controllers. It's imported by assets/app.js and its exact content depends on whether you have Webpack Encore or AssetMapper installed (see below).
  • assets/app.js is updated to import assets/bootstrap.js
  • assets/controllers.json This 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 example hello_controller.js file.

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/bootstrap.js file to look like this:

1
2
3
4
// assets/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.

Tip

For AssetMapper 6.3 only, you also need a {{ ux_controller_link_tags() } in base.html.twig. This is not needed in AssetMapper 6.4+.

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/bootstrap.js file will be updated to look like this:

1
2
3
4
5
6
7
8
9
// assets/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?$/
));

And 2 new packages - @hotwired/stimulus and @symfony/stimulus-bridge - will be added to your package.json file.

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/bootstrap.js file will automatically register:

  • All files in assets/controllers/ as Stimulus controllers;
  • And all controllers described in assets/controllers.json as Stimulus controllers.

Note

If you're using WebpackEncore, the bootstrap.js file works in partnership with @symfony/stimulus-bridge. With AssetMapper, the 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">
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version