Stimulus (estímulo)
Bienvenido al día de suerte número 7. Hoy hablamos de Stimulus: una biblioteca JavaScript pequeña y fácil de entender que nos permite crear código superorganizado que... simplemente siempre funciona. Es una de mis razones favoritas para utilizar Internet.
Instalar StimulusBundle
Pero aunque Stimulus es una librería JavaScript... Symfony tiene un bundle para ayudarnos a cargarla, configurarla y utilizarla. Así que, busca tu terminal y ejecuta:
composer require "symfony/stimulus-bundle:^2.0"
Una de las cosas más importantes de StimulusBundle es su receta. Cuando termine, ejecuta:
git status
La Receta Cambia
Oooh. Ha hecho varios cambios. El primero está aquí, en assets/app.js. Encima -quitaré ese comentario- ahora importamos un nuevo bootstrap.js:
| import './bootstrap.js'; | |
| // ... lines 2 - 16 |
Ese archivo inicia la aplicación Stimulus.
Observa que importa un módulo @symfony/stimulus-bundle:
| import { startStimulusApp } from '@symfony/stimulus-bundle'; | |
| // ... lines 2 - 6 |
El símbolo @ no es importante: es sólo un carácter que utilizan los paquetes JavaScript con espacio de nombres. Lo importante es que se trata de una importación desnuda, lo que significa que el navegador intentará encontrar este paquete mirando nuestro mapa de importación.
Vale Abre importmap.php. La receta ha añadido dos nuevas entradas aquí:
| // ... 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', | |
| ], | |
| ]; |
La primera es para el propio Stimulus, que ahora vive en el directorio assets/vendor/. La segunda es... una especie de paquete "falso" de terceros. Dice que @symfony/stimulus-bundledebe resolver a un archivo en nuestro directorio vendor/. Esto es un poco de fantasía: decimos import '@symfony/stimulus-bundle'... y eso importará en última instancia este archivo loader.js desde vendor/.
La receta también añadió un directorio controllers/ -el hogar de nuestros controladores de Stimulus personalizados- y un archivo controllers.json, del que hablaremos mañana.
Ah, y en base.html.twig, añadió esta línea ux_controller_link_tags():
| <html> | |
| <head> | |
| // ... lines 4 - 7 | |
| {% block stylesheets %} | |
| {{ ux_controller_link_tags() }} | |
| {% endblock %} | |
| // ... lines 11 - 14 | |
| </head> | |
| // ... lines 16 - 47 | |
| </html> |
¡Bórrala! Eso era necesario con AssetMapper 6.3, pero ya no. De todas formas, hablaremos de ello mañana.
Utilizar Stimulus
Vale: todo lo que hemos hecho es composer require este nuevo bundle. Y, sin embargo, cuando actualizamos la página y miramos la consola, ¡Stimulus ya está funcionando! Estosapplication #starting y application #start proceden de Stimulus. Es increíble.
Con StimulusBundle, cualquier cosa que pongamos en el directorio controllers/ estará automáticamente disponible como controlador de Stimulus. Así, el hecho de que tengamos unhello_controller.js significa que podemos utilizar un controlador llamado 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'; | |
| } | |
| } |
De hecho, podemos verlo ahora mismo. Cuando se activa este controlador, sustituye el texto del elemento al que está unido. Para comprobar que Stimulus funciona, inspecciona cualquier elemento de la página... e introduce un data-controller="hello".
Cuando pulse intro, ¡boom! Se activa el controlador.
Crear un controlador personalizado
Ha sido divertido, pero vamos a crear nuestro propio controlador real. Copia hello_controller.jsy crea un nuevo archivo llamado celebrate_controller.js. Eliminaré los comentarios y el método connect:
| import { Controller } from '@hotwired/stimulus'; | |
| // ... lines 2 - 3 | |
| export default class extends Controller { | |
| // ... lines 5 - 8 | |
| } |
Éste es el objetivo: cuando pasemos el ratón por encima del logotipo, quiero llamar a un método del controlador que active la biblioteca js-confetti. Empieza por crear el método. Podría llamarse como quieras, pero poof() ¡seguro que es un nombre divertido!
Dirígete a app.js, copia el código de js-confetti y elimínalo:
| // ... lines 1 - 9 | |
| import JSConfetti from 'js-confetti'; | |
| const jsConfetti = new JSConfetti(); | |
| jsConfetti.addConfetti(); | |
| // ... lines 14 - 16 |
Colócalo en el controlador celebrate... y mueve la declaración de importación a la parte superior:
| import { Controller } from '@hotwired/stimulus'; | |
| import JSConfetti from 'js-confetti'; | |
| export default class extends Controller { | |
| poof() { | |
| const jsConfetti = new JSConfetti(); | |
| jsConfetti.addConfetti(); | |
| } | |
| } |
¡Genial! El último paso es activar esto en un elemento. Hazlo en base.html.twig. Veamos... aquí está el logotipo. Añade data-controller="celebrate". Y para activar la acción al pasar el ratón, di data-action=""... y la sugerencia es casi correcta. El formato es, primero: el evento JavaScript que queremos escuchar. En lugar de click, queremos mouseover. Luego ->, el nombre de nuestro controlador, # y el nombre del método: 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> |
¡Ya está! ¡Refresca y celébralo! Cada vez que mouseover, llama al método. Puedes verlo abundantemente en la consola.
Vaya, en cuanto añadimos un controlador al directorio controllers/, ya está cargado y listo para funcionar. Recuerda, todo sin compilar.
Carga perezosa de controladores
Pero a veces puedes tener un controlador que sólo se utiliza en determinadas páginas... por lo que no quieres obligar a tu usuario a descargarlo inmediatamente en cada página. Si te encuentras en esa situación, puedes hacer que tu controlador sea perezoso. Es lo mejor.
Para ello, añade este comentario especial sobre él: stimulusFetch: 'lazy':
| // ... lines 1 - 3 | |
| /* stimulusFetch: 'lazy' */ | |
| export default class extends Controller { | |
| // ... lines 6 - 9 | |
| } |
Sí, es una locura. Pero en cuanto hagamos eso, en lugar de descargar este archivo al cargar la página, esperará hasta que vea un elemento en la página condata-controller"celebrate".
Observa: borra temporalmente el data-controller. Luego vuelve, actualiza, y en las herramientas de red, si busco celebrate, no hay nada. Si buscoconfetti -desde que nuestro controlador importa- js-confetti, tampoco está. No se han descargado.
Limpia tus herramientas de red. Luego ve al logo y hackea ese data-controller. Estamos imitando lo que ocurriría si cargáramos algo de HTML fresco vía AJAX y... ese HTML fresco incluye un elemento con data-controller"celebrate".
En cuanto eso aparezca en la página, vuelve a las herramientas de red. ¡Aparecieron dos elementos nuevos! Se fijó en el data-controller y descargó el controlador y js-confetti... ya que eso se importa desde el controlador. Y funciona de maravilla. Esto me encanta.
Mantén este controlador perezoso, pero vuelve a base.html.twig, vuelve a añadir data-controller.
¡Una de las grandes cosas de Stimulus es que lo utiliza gente de toda la Interwebs! Y hay muchos controladores Stimulus prefabricados por ahí para ayudarnos a resolver problemas. Uno de ellos se llama Symfony-UX. Mañana nos sumergiremos en uno de sus paquetes.
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!