Stimulus: Writing Pro JavaScript
We know how to write HTML in our templates. And we're handling CSS with Tailwind. What about JavaScript? Well, like with CSS, there's an app.js file, and it's already included on the page. So you can put whatever JavaScript you want right here.
But I highly recommend using a small, but mean, JavaScript library called Stimulus. It is one of my absolute favorite things on the Internet. You take a part of your existing HTML and connect it to a small JavaScript file, called a controller. This allows you to add behavior: like when you click this button, the greet method on the controller will be called.
And that's really it! Sure, Stimulus has more features, but you already understand the core of how it works. Despite its simplicity, this will let us build any JavaScript and user interface functionality we need, in a reliable and predictable way. So let's get it installed.
Installing Stimulus
Stimulus is a JavaScript library, but Symfony has a bundle that helps integrate it. Over at your terminal, if you want to see what the recipe does, commit your changes. I already have. Then run:
composer require symfony/stimulus-bundle
When this finishes... the recipe did make some changes. Let's walk through the important ones. The first is in app.js: our main JavaScript file. Open that up, there we go.
| import './bootstrap.js'; | |
| /* | |
| * Welcome to your app's main JavaScript file! | |
| * | |
| * This file will be included onto the page via the importmap() Twig function, | |
| * which should already be in your base.html.twig. | |
| */ | |
| import './styles/app.css'; | |
| console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉'); |
It added an import on top - ./bootstrap.js - to a new file that lives right next to this.
| import { startStimulusApp } from '@symfony/stimulus-bundle'; | |
| const app = startStimulusApp(); | |
| // register any custom, 3rd party controllers here | |
| // app.register('some_controller_name', SomeImportedController); |
The purpose of this file is to start the Stimulus engine. Also, in importmap.php, the recipe added the @hotwired/stimulus JavaScript package along with another file that helps boot up Stimulus inside Symfony.
| // ... lines 1 - 15 | |
| return [ | |
| // ... lines 17 - 20 | |
| '@hotwired/stimulus' => [ | |
| 'version' => '3.2.2', | |
| ], | |
| '@symfony/stimulus-bundle' => [ | |
| 'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js', | |
| ], | |
| ]; |
Finally, the recipe created an assets/controllers/ directory. This is where our custom controllers will live. And it included a demo controller to get us started! Thanks!
| 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'; | |
| } | |
| } |
These controller files do have an important naming convention. Because this is called hello_controller.js, to connect this with an element on the page, we'll use data-controller="hello".
How Stimulus Works
So here's how this works. As soon as Stimulus sees an element on the page with data-controller="hello", it will instantiate a new instance of this controller and call the connect() method. So, this hello controller should automatically and instantly change the content of the element it's attached to.
And we can already see this. Refresh the page. Stimulus is now active on our site. This means it's watching for elements with data-controller. Let's do something wild: inspect element on the page, find any element - like this anchor tag - and add data-controller="hello". Watch what happens when I click off to activate this change. Boom! Stimulus saw that element, instantiated our controller and called the connect() method. And you can do this as many times as you want on the page.
The point is: no matter how a data-controller element get on your page, Stimulus sees it. So if we make an Ajax call that returns HTML and put that onto the page... yeah, Stimulus is going to see that and our JavaScript is going to work. That's the key: when you write JavaScript with Stimulus, your JavaScript will always work, no matter how and when that HTML is added to the page.
Creating a closeable Stimulus Controller
So let's use Stimulus to power our close button. Over in the assets/controller/ directory, duplicate hello_controller.js and make a new one called closeable_controller.js.
I'll clear out almost everything and get down to the absolute basics: import Controller from stimulus... then create a class that extends it.
| import { Controller } from '@hotwired/stimulus'; | |
| export default class extends Controller { | |
| // ... lines 4 - 6 | |
| } |
This doesn't do anything, but we can already attach it to an element on the page. Here's the plan: we're going to attach the controller to the entire aside element. Then, when we click this button, we'll remove the aside.
That element lives over in templates/main/_shipStatusAside.html.twig. To attach the controller, add data-controller="closeable". Oh, see that autocompletion? That comes from a Stimulus plugin for PhpStorm.
| <aside | |
| // ... line 2 | |
| data-controller="closeable" | |
| > | |
| // ... lines 5 - 35 | |
| </aside> |
If we move over and refresh, nothing will happen yet: the close button doesn't work. But open your browser's console. Nice! Stimulus adds helpful debugging messages: that it's starting and then - importantly closeable initialize, closeable connect.
This means that it did see the data-controller element and initialized that controller.
So back to our goal: when we click this button, we want to call code inside the closeable controller that will remove the aside. In closeable_controller.js, add a new method called, how about, close(). Inside, say this.element.remove().
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| close() { | |
| this.element.remove(); | |
| } | |
| } |
In Stimulus, this.element will always be whatever element your controller is attached to. So, this aside element. But otherwise, this code is standard JavaScript: every Element has a remove() method.
To call the close() method, on the button, add data-action="" then the name of our controller - closeable - a # sign, and the name of the method: close.
| <aside | |
| // ... line 2 | |
| data-controller="closeable" | |
| > | |
| <div class="flex justify-between mt-11 mb-7"> | |
| // ... line 6 | |
| <button data-action="closeable#close"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 448 512"><!--!Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path fill="#fff" d="M384 96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V96zM9.4 278.6c-12.5-12.5-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L109.3 224 288 224c17.7 0 32 14.3 32 32s-14.3 32-32 32l-178.7 0 73.4 73.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0l-128-128z"/></svg> | |
| </button> | |
| </div> | |
| // ... lines 11 - 35 | |
| </aside> |
Animating the Close
That's it! Testing time. Click! Gone! But I want it be fancier! I want it to animate when closing instead of being instant. Can we do that? Sure! And we don't need much JavaScript... because modern CSS is amazing.
Over on the aside element, add a new CSS class - it could go anywhere - called transition-all.
That's a Tailwind class that activates CSS transitions. This means that if certain style properties change - like the width suddenly being set to 0 - it will transition that change, instead of instantly changing it.
Also add overflow-hidden so that, as the width gets smaller, it doesn't create a weird scroll bar.
If we try this now, it still closes instantly. That's because there's nothing to transition: we're not changing the width... just removing the element.
But watch this. Inspect Element and find the aside: here it is. Manually change the width to 0. Cool! You go tiny, big, tiny, big, tiny! The CSS side of things is working.
Back in our controller, instead of removing the element, we need to change the width to zero, wait for the CSS transition to finish, then remove the element. We can do the first with this.element.style.width = 0.
| <aside | |
| // ... line 2 | |
| data-controller="closeable" | |
| > | |
| <div class="flex justify-between mt-11 mb-7"> | |
| // ... line 6 | |
| <button data-action="closeable#close"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 448 512"><!--!Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path fill="#fff" d="M384 96c0-17.7 14.3-32 32-32s32 14.3 32 32V416c0 17.7-14.3 32-32 32s-32-14.3-32-32V96zM9.4 278.6c-12.5-12.5-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3L109.3 224 288 224c17.7 0 32 14.3 32 32s-14.3 32-32 32l-178.7 0 73.4 73.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0l-128-128z"/></svg> | |
| </button> | |
| </div> | |
| // ... lines 11 - 35 | |
| </aside> |
The tricky part is waiting for the CSS transition to finish before removing the element. To help with that, I'm going to paste a method at the bottom of our controller.
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| async close() { | |
| this.element.style.width = '0'; | |
| // ... lines 6 - 8 | |
| } | |
| #waitForAnimation() { | |
| return Promise.all( | |
| this.element.getAnimations().map((animation) => animation.finished), | |
| ); | |
| } | |
| } |
If you're not familiar, the # sign makes this a private method in JavaScript: a small detail. This code looks fancy, but it has a simple job: to ask the element to tell us when all of its CSS animations are finished.
Thanks to that, up here, we can say await this.#waitForAnimation(). And whenever you use await, you need to put async on the function around this. I won't go into details about async, but that won't change how our code works.
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| async close() { | |
| this.element.style.width = '0'; | |
| await this.#waitForAnimation(); | |
| this.element.remove(); | |
| } | |
| #waitForAnimation() { | |
| return Promise.all( | |
| this.element.getAnimations().map((animation) => animation.finished), | |
| ); | |
| } | |
| } |
Let's check the result! Refresh. And... I absolutely love that.
Next up, everyone wants a single page application, right? A site where there are zero full page refreshes. But to build one, don't we need to use a JavaScript framework like React? No! We're going to transform our app into a single page application in... about 3 minutes with Turbo.
24 Comments
Nice and very interesting tutorial.
One question, how do you bring back the aside part of the page after hiding it ?
In the video, you click the arrow button once to trigger the
closeable#closemethod, and then looks like you click again the screen to bring back the aside div. I'm doing so to not avail or am I missing something ?BTW, how is doing Ryan ?
Hey @symfonyace ,
Yeah, good question, because it's probably not very clear from the video. Actually, Ryan just reloads the page after the sidebar is gone i.e. he clicks the button, the sidebar is animated and removed completely, then he reloads the page in the browser (presses a shortcut like Cmd + R on a Mac to reload the page) that loads a new HTML page again with the sidebar, and finally clicks the button again to remove the sidebar. And so on, just super simple and no magic ;)
Thank you for asking about Ryan! Unfortunately, Ryan passed away last year. You can read the related blog post about him here: https://symfonycasts.com/blog/remembering-ryan-weaver
Cheers!
I am trying to follow this course in 2026, and I do not see the _shipStatusAside.html.twig. What am I missing? This is very hard to follow to be honest.
Update: I found the "aside" element in homepage.html.twig and was able to continue the video.
Also, let me just add that not everybody is using PHPStorm. I am using VSCode and not all that functionality you show off is available here. I feel like this course should focus on purely Symfony and not utilize some features PHPStorm includes. I get it: it is the defacto IDE for PHP. Just know that some of us just learning Symfony did not pay for PHPStorm and are using freely available resources instead. And I had to heavily add extensions to VSCode to even follow this course from the beginning. Just letting my voice heard.
Hey Kay,
I'm happy to see you were able to find that youself, good job!
Thank you for your interest in SymfonyCasts tutorials! Yes, you're totally right, not all devs use PhpStorm for their own reasons, also it's just a matter of taste too. Though we would like to recommend it as the best IDE for PHP and Symfony projects. We're trying to avoid using specific PhpStorm features unless it helps to save some time in the video, and when users should be clear enough on how to do that manually. We constantly try to keep it in mind, and I believe you should not encounter such moments in the tutorials very often.
Anyway, thanks again for reminding that, we appreciate it. And I agree with you on this. If you ever have any problems following our tutorials - please, feel free to ask your questions in the comments section below the video and our support team will help you :)
Cheers!
OMG! I just realized that for some reason, I jumped ahead to chapter 18 instead of doing chapter 15! Idk how that happened, I'm so sorry. I was just following along the track and it landed me on Chapter 18, and I didn't check!
You ARE creating that partial twig file in Chapter 15 that I was kinda missing in this video. Urgh!!! Again, I'm so sorry.
Hey Kay,
Oh, no problem! That may happen if you open that Ch18 in a new tab and play the video, then the system could return you back to that chapter when you ocntinue the course. Or it was just an accidental click that may happen too. Anyway, I'm glad to hear you're back on the right path ;)
If you ever have any questions following our tutorials - please, just ask!
Cheers!
Much appreciated. Thanks!
Any help your side sockets io how to use in symfony how to install example npm install express@4
Hey Himanshu,
If you're talking about web sockets - we have a few screencasts about Mercure Symfony component, kind of like web sockets but cooler :) You can check them here: https://symfonycasts.com/search?q=mercure&sort=most relevant&types[]=video - you can leverage our advanced search on SymfonyCasts to find more content related to a specific stack. Unfortunately, no content about express@4.
I hope it helps!
Cheers!
The
preloadoption is now obsoleted inimportmap.phpfile on the version 7.2 of Symfony ?It’s no longer mentioned in the multi-line comment.
Hey Rom,
I don't see we use that on this tutorial, or am I missing something? If so, please, could you link me to the correct chapter? Also, could you tell me what exactly deprecation message do you see about that preload option? Or do you mean it's gone from the recipe now? If so, IIRC that should be enabled by default now, you can check the source HTML code to see it includes preload. If not, you can always enable it manually in the importmap youself.
Cheers!
Honestly, I feel that there is a bit too much info for introduction tutorial. Enum, stimulus, tailwind... zis is all a bit overwhelming.
Hey @John-S
Thank you for your feedback and I agree with you, all of this can be too much. By the way, we have a dedicated tutorial for Stimulus and Symfony UX https://symfonycasts.com/screencast/stimulus
Cheers!
first of all thank you for these fabulous and very precious courses for those like me who want to learn more, I have a question (perhaps I have already asked it in another post) but a few months ago I remember that you had published a recent video course regarding the concept of asset manager which seemed be the last frontier by putting stimulus and turbo on the bench, am I remembering correctly? My memory recalls the confetti script example. If I don't remember correctly, I ask for forgiveness. I would like to understand, nowadays what is the best method to manage attractive and latest generation UX? Thanks for all ;)
Hey @pasquale_pellicani ,
It depends on your needs, the project, and your knowledge. We do recommend Stimulus/Turbo as it's a straightforward tool with a nice Symfony integration. This is just ideal if you have a server-side project and want to give it a feeling of single-page application. It's ideal if you're OK with rendering templates server-side and let Stimulus handle it for you. In short, you just need to understand Stimulus, it's basic idea and how it works. For this - we do recommend you to watch this Stimulus course :)
But if Stilulus isn't enough for your needs, i.e. if you need more complex UI, if you want to render UI client-side without sending AJAX requests to the server and get already rendered template response that Stimulus just insert into the page for you - then take a look at more serious UI frameworks like VueJS, or React/Angular for example. They all are kinda good, and if you already know something well - probably better to use that as you will spend more time on developing instead of learning and fixing some edge cases when you start with a completely new technology for you.
So that's my personal opinion on it. In. short, if Stimulus and Turbo is enough for you - great, definitely go with it. If not - look at something more serious and focused on client-side rendering like Vue/React/Angular.
I hope that helps!
Cheers!
thanks you always come for the precious answers @Victor , so what do I do? Am I waiting for a reactJs + Symfony video course? ;)
Hey @pasquale_pellicani ,
Haha, we're mostly leaning towards VueJS instead of ReactJS, and we do have some screencasts about it here:
We will definitely want to record more Vue-related content but I don't know when it might be released, we're busy with other good topics lately.
About ReactJS, we worked with it before, and even have a course: https://symfonycasts.com/screencast/reactjs - but then we re-wrote that part of the system with Stimulus instead. and it works great in our specific case :)
Cheers!
So, why would one choose Vue or React or stimulus? What do you think the choice is based on?
Is this the most recent guide on the stimulus/turbo topic? Or are there more recent ones?
thanks
Hey @pasquale_pellicani ,
That https://symfonycasts.com/screencast/last-stack course is the latest indeed, but it includes more topics besides Stimulus & Turbo. If you're looking for a Stimulus and Turbo course in more details, I would recommend you to watch this topic-specific courses:
Well, first of all, Stimulus is not the same as Vue/React, so technically the difference is the same I mentioned above. If you're looking for client-side UI rendering - take a look at Vue/React. If you're OK with rendering UI server-side - Stimulus might be enough for you, it will help you dynamically inject. already server-side rendered templates into the page via AJAX requests.
So if simplify that to "Vue or React"? Well, simply, a matter of taste mostly. Something gets more hipster with a new release, etc. Sometimes people start working with something and so are already familiar with the technology and it makes sense to continue working with it while it's doing the job for you. On the more technical level and limitations - you probably first should get familiar with both technologies so that you could make some conclusions yourself.
I hope that helps!
Cheers!
Hello,
There is problem on my Stimulus that it is now showing output of java script. My project is working under ubuntu apache web server.
Below is my base config and I added {{stimulus_controller ('hello')}} to twig. could you please help to troubleshoot this problem? thank you
Hey Mahmut,
Are you following this course on your private project you did you download the course code and started from the start/ directory? It feels like you're on a private project. Please, download the course code and look at the base.html.twig - that contains some blocks, e.g.:
That are required for Stimulus to work, as they integrate Stimulus code into your project.
Cheers!
thank you.I noticed it and after adding block javascript, it worked.
Hey Mahmut,
Great, thanks for confirming
javascriptblock helped!Cheers!
"Houston: no signs of life"
Start the conversation!