Stimulus
Welcome to lucky day number 7. Today we're talking about Stimulus: a small, easy-to-understand JavaScript library that lets us create super-organized code that... just always works. It is one of my favorite reasons to use the Internet.
Installing StimulusBundle
But even though Stimulus is a JavaScript library... Symfony has a bundle to help us load it, get it set up, and use it. So, find your terminal and run:
composer require "symfony/stimulus-bundle:^2.0"
One of the most important things about StimulusBundle is its recipe. After it finishes, run:
git status
The Recipe Changes
Oooh. It made a number of changes. The first is over here in assets/app.js. On top - I'll remove that comment - we're now importing a new bootstrap.js:
| import './bootstrap.js'; | |
| // ... lines 2 - 16 |
That file starts the Stimulus application.
Notice that this imports an @symfony/stimulus-bundle module:
| import { startStimulusApp } from '@symfony/stimulus-bundle'; | |
| // ... lines 2 - 6 |
The @ symbol isn't important: that's just a character namespaced JavaScript packages use. The important thing is that this is a bare import, which means the browser will try to find this package by looking at our importmap.
Ok! Open up importmap.php. The recipe added two new entries here:
| // ... lines 1 - 15 | |
| return [ | |
| // ... lines 17 - 23 | |
| '@hotwired/stimulus' => [ | |
| 'version' => '3.2.2', | |
| ], | |
| '@symfony/stimulus-bundle' => [ | |
| 'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js', | |
| ], | |
| ]; |
The first is for Stimulus itself - that now lives in the assets/vendor/ directory. The second is... a kind of "fake" 3rd party package. It says that @symfony/stimulus-bundle should resolve to a file in our vendor/ directory. This is a bit of fanciness: we say import '@symfony/stimulus-bundle'... and that will ultimately import this loader.js file from vendor/.
The recipe also added a controllers/ directory - the home for our custom Stimulus controllers - and a controllers.json file, which we'll talk about tomorrow.
Oh, and in base.html.twig, it added this ux_controller_link_tags() line:
| <html> | |
| <head> | |
| // ... lines 4 - 7 | |
| {% block stylesheets %} | |
| {{ ux_controller_link_tags() }} | |
| {% endblock %} | |
| // ... lines 11 - 14 | |
| </head> | |
| // ... lines 16 - 47 | |
| </html> |
Delete it! That was needed with AssetMapper 6.3, but not anymore. We'll talk about what that did tomorrow anyway.
Using Stimulus
Ok: so, all we've done is composer require this new bundle. And yet, when we refresh the page and look at the console, Stimulus is already working! This application #starting and application #start come from Stimulus. That's awesome.
With StimulusBundle, anything we put into the controllers/ directory will automatically be available as a Stimulus controller. So, the fact that we have a hello_controller.js means that we can use a controller named hello:
| import { Controller } from '@hotwired/stimulus'; | |
| /* | |
| * This is an example Stimulus controller! | |
| * | |
| * Any element with a data-controller="hello" attribute will cause | |
| * this controller to be executed. The name "hello" comes from the filename: | |
| * hello_controller.js -> "hello" | |
| * | |
| * Delete this file or adapt it for your use! | |
| */ | |
| export default class extends Controller { | |
| connect() { | |
| this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js'; | |
| } | |
| } |
In fact, we can see it right now. When this controller is activated, it replaces the text of the element it's attached to. To prove Stimulus is working, inspect any element on the page... and hack in a data-controller="hello".
When I hit enter, boom! It activates the controller.
Creating a Custom Controller
That was fun, but let's create our own, real controller. Copy hello_controller.js and create a new file called celebrate_controller.js. I'll remove the comments and the connect method:
| import { Controller } from '@hotwired/stimulus'; | |
| // ... lines 2 - 3 | |
| export default class extends Controller { | |
| // ... lines 5 - 8 | |
| } |
Here's the goal: when we hover over the logo, I want to call a method on the controller that triggers the js-confetti library. Start by creating the method. It could be called anything, but poof() sure is a fun name!
Head over to app.js, copy the js-confetti code and delete it:
| // ... lines 1 - 9 | |
| import JSConfetti from 'js-confetti'; | |
| const jsConfetti = new JSConfetti(); | |
| jsConfetti.addConfetti(); | |
| // ... lines 14 - 16 |
Pop that into celebrate controller... and move the import statement to the top:
| import { Controller } from '@hotwired/stimulus'; | |
| import JSConfetti from 'js-confetti'; | |
| export default class extends Controller { | |
| poof() { | |
| const jsConfetti = new JSConfetti(); | |
| jsConfetti.addConfetti(); | |
| } | |
| } |
Cool! The last step is to activate this on an element. Do that in base.html.twig. Let's see... here's the logo. Add data-controller="celebrate". And to trigger the action on hover, say data-action=""... and the suggestion is almost correct. The format is, first: the JavaScript event that we want to listen to. Instead of click, we want mouseover. Then ->, the name of our controller, # and the method name: poof:
| // ... line 1 | |
| <html> | |
| // ... lines 3 - 14 | |
| <body class="bg-black text-white font-mono"> | |
| <div class="container mx-auto min-h-screen flex flex-col"> | |
| <header class="my-8 px-4"> | |
| <nav class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center"> | |
| <a | |
| href="{{ path('app_homepage') }}" | |
| data-controller="celebrate" | |
| data-action="mouseover->celebrate#poof" | |
| > | |
| <img src="{{ asset('images/logo.png') }}" width="50" alt="Space Inviters Logo" > | |
| </a> | |
| // ... lines 27 - 29 | |
| </div> | |
| // ... lines 31 - 36 | |
| </nav> | |
| </header> | |
| // ... lines 39 - 48 | |
| </div> | |
| </body> | |
| </html> |
That's it! Refresh and celebrate!!! Each time we mouseover, it calls the method. You can see this liberally in the console.
Wow, so, as soon as we add a controller to the controllers/ directory, it's loaded and ready to go. Remember, all with no build.
Lazy-Loading Controllers
But sometimes you might have a controller that's only used on certain pages... so you don't want to force your user to download it immediately on every page. If you have that situation, you can make your controller lazy. It's the best.
To do that, add this special comment above it: stimulusFetch: 'lazy':
| // ... lines 1 - 3 | |
| /* stimulusFetch: 'lazy' */ | |
| export default class extends Controller { | |
| // ... lines 6 - 9 | |
| } |
Yes, that is pretty crazy. But as soon as we do that, instead of downloading this file on page load, it will wait until it sees an element on the page with data-controller"celebrate".
Watch: delete the data-controller temporarily. Then go over, refresh, and on the network tools, if I search for celebrate, there's nothing there. If I search for confetti - since our controller imports - js-confetti, that's also not there. Those have not been downloaded.
Clear out your network tools. Then go up to the logo and hack in that data-controller. We're imitating what would happen if we loaded some fresh HTML via AJAX and... that fresh HTML includes an element with data-controller"celebrate".
As soon as that appears on the page, go back to the network tools. Two new items showed up! It noticed the data-controller and downloaded the controller and js-confetti... since that's imported from the controller. And it works brilliantly. I absolutely love this.
Keep this controller lazy, but back in base.html.twig, re-add data-controller.
One of the great things about Stimulus is that it's used by people all over the Interwebs! And there are many pre-made Stimulus controllers out there to help us solve problems. One group of them is called Symfony-UX. We'll dive into one of its packages tomorrow.
17 Comments
It's also possible to use the Twig helper functions for Stimulus controllers, actions (and targets).
See: https://symfony.com/bundles/StimulusBundle/current/index.html#stimulus-twig-helpers
For the example in this chapter this would be:
Hey @Coding010 ,
Thanks for this tip! Indeed, you can leverage Twig helper functions but those are not considered best practices anymore, see this PR for more information: https://github.com/symfony/ux/pull/1336
Cheers!
Thanks, I was not aware of this.
Hey Coding010,
Yeah, that's something that was changed recently. But it's mostly just a matter of taste, so you can use whatever you like :)
Cheers!
Hello to the awesome team,
I've been a bit lost for the past few days because the link to the homepage no longer works since I used AssetMapper. I'm using a spinner that keeps looping because my main.js file isn't loaded when I click the home button (console.log("hidden spinner")) works on page load but not afterward).
My project is running on Symfony 7.2 and PHP 8.3. I created it using symfony new project... --webapp.
When I inspect the browser (Chrome), main.js is indeed present in the list (but not in preload). I don't know how to add my main.js in prealo (if it's the issue).
Thank you for your help.
Hey @Delenclos-A
Do you have Turbo enabled? When you click on a link it won't trigger a full page reload, it makes an AJAX request and updates the HTML. Also, double check that your
main.jsis included on your page. In this chapter you can see how to deal with JS modules https://symfonycasts.com/screencast/last-stack/js-modules#commentsCheers!
Thank you so much. Turbo was enabled in controllers.json. Now it works fine. That's great!
Hi All,
For me without
import { startStimulusApp } from '@symfony/stimulus-bundle';i'll have error :)Hey Amine,
Yes, that import is required for the Stimulus bundle to work, and we do have that in the screencast, right? I mean, I don't see in the screencast that we're removing that line, let me know if I'm missing something.
Cheers!
After following along with the video I get this error:
Asset with public path "/assets/@symfony/stimulus-bundle/controllers.js" not found
Any suggestions as to why? I used to use Webpack Encore but I believe I have successfully removed it and am trying to use Asset Mapper. Thank you.
Hey @Brandon!
Let's see if we can figure this out :). To confirm, you upgraded from a Webpack Encore project to AssetMapper, right? If so, just as a reference, if you want to compare to another upgrade, check out https://github.com/symfony/demo/pull/1449
Anyway, hmm. So here is the process:
assets/bootstrap.js, your code should look like this https://github.com/symfony/demo/blob/31d5da9f43201982c9c8cb422dcb4beafd24803b/assets/bootstrap.js@symfony/stimulus-bundlereferences a new entry in yourimportmap.phpfile - https://github.com/symfony/demo/blob/main/importmap.php#L47-L49. As you can see, what you're really importing is avendor/symfony/stimulus-bundle/assets/dist/loader.jsfile. Normally, only files inassets/are available publicly. But StimulusBundle makes its ownassets/directory public as well. You can see this when you rundebug:assetloader.jsfile then importscontrollers.js- https://github.com/symfony/ux/blob/a3de399f590270cd74f1895b4ceeccefd48fe50b/src/StimulusBundle/assets/dist/loader.js#L2So, the mystery is: why isn't that file being found? When you say:
Is this an error in your browser console? Or an error from Symfony? What's especially curious is that the
loader.jsfile seems to be found from the bundle and loaded... but notcontrollers.js.Let me know if any of these pointers or questions help... or at least lead to more answers. I can't quite spot the problem yet...
Cheers!
Ryan,
I followed the process you listed before typing my question and oddly enough what I did was open loader.js and at this line:
import { isApplicationDebug, eagerControllers, lazyControllers } from './controllers.js';
I removed the '.' before ./controllers.js', reloaded my site, got the error it couldn't find it, added the '.' back and it worked. I'm not sure as to why this happened, I cleared my cache before all of this as well. I am moving from Encore to Asset Mapper, I was never in love with Encore when I first started with it. I will definitely watch your video, that sounds exactly like what I need to see. With something like jquery-ui, I added jquery first, and I'm importing $ from jquery in my certain controllers that require it, but when I use datepick from jquery-ui, I get the jQuery not defined. Do I need to import jquery and $ from jquery all before datepicker?
Hey @Brandon!
Hmm, that's super weird. It does smell like a cache issue (we store cache internally in
var/cache/in dev to help not constantly recompile things) even though you said that you cleared the cache. By tweaking that file, you invalidated its cache... but who knows :).jQuery plugins can be a pain. Most of the time, they are written well and will try to
import $ from 'jquery'inside. But some expectjQueryas a global variable. If you have this case, you need to work around by setting a global variable. For example:1) Create a file that sets a global variable - https://github.com/symfony/demo/blob/main/assets/js/jquery_global.js
2) Import it before you need it - https://github.com/symfony/demo/blob/main/assets/admin.js#L4-L6 - in this case,
bootstrap-tagsinputneeds a global variable.Hopefully, however, we can start to move away from jQuery. For normal things like selecting elements, using Stimulus and normal JS is really, really nice and easy these days. For things like a datepicker, I'd love to ship/share some Stimulus controllers that add this functionality.
Cheers!
How do I register Stimulus Controllers from vendor/mybundle/assets/controllers. Could you please help.
For anyone else - question answered over here :) https://symfonycasts.com/screencast/stimulus/controllers#comment-30971
hi @weaverryan,
how do you have suggestion in data-action ?
I have Symfony support (freemium version) and stimulus plugins installed in phpStorm but data-action only propose the controllers in assets/controllers
thanks!
Hey @Cedric!
I'm 98% sure I'm getting that from the PhpStorm Stimulus plugin (in fact, someone else asked the same thing and someone else gave them the same answer! https://symfonycasts.com/screencast/last-stack/ux-packages#comment-30981
From my experience, it first looks to make find a
data-controllerin a higher element (so if thedata-controlleris in some other template that includes this template, I thinkdata-actionwon't work). Also, I believe you need to typedata-action="celebrate#p"and actually type the first letter of the action before it shows up. I could be wrong about that - but iirc, it doesn't autocomplete directly after typing the#, which is a shame.So... my answer is... via something you already have 😆. But maybe this will help.
Cheers!
"Houston: no signs of life"
Start the conversation!