Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Ejemplo de Stimulus en el mundo real

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Pongamos a prueba a Stimulus. Éste es nuestro objetivo: cuando hagamos clic en el icono de reproducción, haremos una petición Ajax a nuestra ruta de la API... la que está en SongController. Esto devuelve la URL donde se puede reproducir esta canción. Entonces usaremos eso en JavaScript para... ¡reproducir la canción!

Toma hello_controller.js y cámbiale el nombre a, qué tal song-controls_controller.js. Dentro, sólo para ver si esto funciona, en connect(), registra un mensaje. El métodoconnect() se llama cada vez que Stimulus ve un nuevo elemento coincidente en la página.

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() {
console.log('I just appeared into existence!');
}
}

Ahora, en la plantilla, hola ya no va a funcionar, así que quita eso. Lo que quiero hacer es rodear cada fila de canciones con este controlador.... así que es este elementosong-list. Después de la clase, añade {{ stimulus_controller('song-controls') }}.

{% extends 'base.html.twig' %}
{% block title %}Create a new Record | {{ parent() }}{% endblock %}
{% block body %}
<div class="container">
... lines 7 - 36
{% for track in tracks %}
<div class="song-list" {{ stimulus_controller('song-controls') }}>
... lines 39 - 50
</div>
{% endfor %}
... lines 53 - 55
</div>
{% endblock %}

Vamos a probarlo Actualiza, comprueba la consola y... ¡sí! Golpeó nuestro código seis veces! Una vez por cada uno de estos elementos. Y cada elemento recibe su propia instancia de controlador, por separado.

Añadir acciones de Stimulus

Bien, a continuación, cuando hagamos clic en reproducir, queremos ejecutar algún código. Para ello, podemos añadir una acción. Tiene este aspecto: en la etiqueta a, añade {{ stimulus_action() }} -otra función de acceso directo- y pásale el nombre del controlador al que estás adjuntando la acción - song-controls - y luego un método dentro de ese controlador que debe ser llamado cuando alguien haga clic en este elemento. ¿Qué te parece play.

{% extends 'base.html.twig' %}
{% block title %}Create a new Record | {{ parent() }}{% endblock %}
{% block body %}
<div class="container">
... lines 7 - 36
{% for track in tracks %}
<div class="song-list" {{ stimulus_controller('song-controls') }}>
<div class="d-flex mb-3">
<a href="#" {{ stimulus_action('song-controls', 'play') }}>
<i class="fas fa-play me-3"></i>
</a>
... lines 43 - 49
</div>
</div>
{% endfor %}
... lines 53 - 55
</div>
{% endblock %}

Genial, ¿no? De vuelta en el controlador de la canción, ya no necesitamos el método connect(): no tenemos que hacer nada cada vez que veamos otra fila song-list. Pero sí necesitamos un método play().

Y al igual que con los escuchadores de eventos normales, éste recibirá un objeto event... y entonces podremos decir event.preventDefault() para que nuestro navegador no intente seguir el clic del enlace. Para probar, console.log('Playing!').

import { Controller } from '@hotwired/stimulus';
... lines 2 - 11
export default class extends Controller {
play(event) {
event.preventDefault();
console.log('Playing!');
}
}

¡Vamos a ver qué pasa! Actualiza y... haz clic. Ya funciona. Así de fácil es enganchar un oyente de eventos en Stimulus. Ah, y si inspeccionas este elemento... esa funciónstimulus_action() es sólo un atajo para añadir un atributo especial data-actionque Stimulus entiende.

Instalar e importar Axios

Bien, ¿cómo podemos hacer una llamada Ajax desde dentro del método play()? Bueno, podríamos utilizar la función integrada fetch() de JavaScript. Pero en su lugar, voy a instalar una biblioteca de terceros llamada Axios. En tu terminal, instálala diciendo:

yarn add axios --dev

Ahora sabemos lo que hace: descarga este paquete en nuestro directorio node_modules, y añade esta línea a nuestro archivo package.json.

Ah, y nota al margen: puedes utilizar absolutamente jQuery dentro de Stimulus. No lo haré, pero funciona muy bien - y puedes instalar - e importar - jQuery como cualquier otro paquete. Hablamos de ello en nuestro tutorial de Stimulus.

Bien, ¿cómo utilizamos la biblioteca axios? Importándola

Al principio de este archivo, ya hemos importado la clase base Controller destimulus. Ahora import axios from 'axios'. En cuanto lo hagamos, Webpack Encore cogerá el código fuente de axios y lo incluirá en nuestros archivos JavaScript construidos.

... lines 1 - 11
import axios from 'axios';
... lines 13 - 21

Ahora, aquí abajo, podemos decir axios.get() para hacer una petición GET. Pero... ¿qué debemos pasar para la URL? Tiene que ser algo como /api/songs/5... pero ¿cómo sabemos cuál es el "id" de esta fila?

Valores de Stimulus

Una de las cosas más interesantes de Stimulus es que te permite pasar valores de Twig a tu controlador Stimulus. Para ello, declara qué valores quieres permitir que se pasen a través de una propiedad estática especial: static values = {}. Dentro, vamos a permitir que se pase un valor de infoUrl. Me acabo de inventar ese nombre: creo que pasaremos la URL completa a la ruta de la API. Establece esto como el tipo que será. Es decir, un String.

Aprenderemos cómo pasamos este valor desde Twig a nuestro controlador en un minuto. Pero como tenemos esto, abajo, podemos referenciar el valor diciendo this.infoUrlValue.

... lines 1 - 11
import axios from 'axios';
... line 13
export default class extends Controller {
static values = {
infoUrl: String
}
... line 18
play(event) {
... lines 20 - 21
console.log(this.infoUrlValue);
//axios.get()
}
}

Entonces, ¿cómo lo pasamos? De vuelta en homepage.html.twig, añade un segundo argumento a stimulus_controller(). Este es un array de los valores que quieres pasar al controlador. Pasa a infoUrl el conjunto de la URL.

Hmm, pero tenemos que generar esa URL. ¿Esa ruta tiene ya un nombre? No, añade name: 'api_songs_get_one'.

<?php
... lines 3 - 10
class SongController extends AbstractController
{
#[Route('/api/songs/{id<\d+>}', methods: ['GET'], name: 'api_songs_get_one')]
public function getSong(int $id, LoggerInterface $logger): Response
{
... lines 16 - 27
}
}

Perfecto. Copia eso... y de nuevo en la plantilla, establece infoURl a path(), el nombre de la ruta... y luego una matriz con cualquier comodín. Nuestra ruta tiene un comodínid.

En una aplicación real, estas rutas probablemente tendrían cada una un id de base de datos que podríamos pasar. Todavía no lo tenemos... así que para, en cierto modo, falsear esto, voy a utilizarloop.index. Esta es una variable mágica de Twig: si estás dentro de un bucle de Twig for, puedes acceder al índice -como 1, 2, 3, 4- utilizando loop.index. Así que vamos a usar esto como una identificación falsa. Ah, y no olvides decir id: y luegoloop.index.

... lines 1 - 4
{% block body %}
<div class="container">
... lines 7 - 36
{% for track in tracks %}
<div class="song-list" {{ stimulus_controller('song-controls', {
infoUrl: path('api_songs_get_one', { id: loop.index })
}) }}>
... lines 41 - 52
</div>
{% endfor %}
... lines 55 - 57
</div>
{% endblock %}

¡Hora de probar! Refresca. Lo primero que quiero que veas es que, cuando pasamosinfoUrl como segundo argumento a stimulus_controller, lo único que hace es dar salida a un atributo muy especial data que Stimulus sabe leer. Así es como se pasa un valor a un controlador.

Haz clic en uno de los enlaces de reproducción y... lo tienes. ¡A cada objeto controlador se le pasa su URL correcta!

Hacer la llamada Ajax

¡Vamos a celebrarlo haciendo la llamada Ajax! Hazlo con axios.get(this.infoUrlValue) -sí, acabo de escribirlo-, .then() y una devolución de llamada utilizando una función de flecha que recibirá un argumento response. Esto se llamará cuando termine la llamada Ajax. Registra la respuesta para empezar. Ah, y corrige para usar this.infoUrlValue.

import { Controller } from '@hotwired/stimulus';
... lines 3 - 11
import axios from 'axios';
... line 13
export default class extends Controller {
... lines 15 - 18
play(event) {
event.preventDefault();
axios.get(this.infoUrlValue)
.then((response) => {
console.log(response);
});
}
}

Muy bien, actualiza... ¡y haz clic en el enlace de reproducción! ¡Sí! Ha volcado la respuesta... y una de sus claves es data... ¡que contiene el url!

¡Es hora de dar la vuelta de la victoria! De vuelta a la función, podemos reproducir ese audio creando un nuevo objeto Audio -es un objeto JavaScript normal-, pasándoleresponse.data.url... y llamando a continuación a play().

import { Controller } from '@hotwired/stimulus';
... lines 2 - 11
import axios from 'axios';
export default class extends Controller {
... lines 15 - 18
play(event) {
event.preventDefault();
axios.get(this.infoUrlValue)
.then((response) => {
const audio = new Audio(response.data.url);
audio.play();
});
}
}

Y ahora... cuando le demos al play... ¡por fin! Música para mis oídos.

Si quieres aprender más sobre Stimulus - esto ha sido un poco rápido - tenemos un tutorial entero sobre ello... y es genial.

Para terminar este tutorial, vamos a instalar otra biblioteca de JavaScript. Ésta hará que nuestra aplicación se sienta instantáneamente como una aplicación de una sola página. Eso a continuación.

Leave a comment!

17
Login or Register to join the conversation
Estelle G. Avatar
Estelle G. Avatar Estelle G. | posted hace 8 meses

Why did I not see that Rickroll coming !!!! Well played ^^

7 Reply

You're welcome ;)

Reply
Santosh kalwar Avatar
Santosh kalwar Avatar Santosh kalwar | posted hace 8 meses

What is this "infoUrlValue?" and why we have to use that instead of "infoUrl"

2 Reply

Hey Appketo,

That is "Stimulus Values" concept. Please, rewatch this video one more time if you didn't get it. But in short, you are able to pass some values from the template to the Stimulus controller via something called "values". But to get a passed Stimulus Value you need to add the "Value" prefix to it, e.g. like in this case with infoUrlValue, i.e. this.infoUrlValue.

So, you just need to get used to this "Stimulus syntax", it's standartized thing by it.

I hope it clarifies things a bit more for you!

Cheers!

Reply
Santosh kalwar Avatar
Santosh kalwar Avatar Santosh kalwar | Victor | posted hace 8 meses

Okay, thank you for the explanation. So something like this https://stimulus.hotwired.d...

1 Reply

Hey Appketo,

Yes, exactly. Looks like you found this feature described in docs, good catch!

Cheers!

3 Reply
Filip J. Avatar
Filip J. Avatar Filip J. | posted hace 8 meses

Good old rickroll, and that was loud as well! :D I'm not sure if it's been reported before or not, but there is an issue with pasting text into this input field, it adds empty rows. With each paste it gets progressively worse. Something to keep in mind.

Great stuff so far, although throughout this course I've been screaming at my monitor: "Where are Models? Surely they're coming any moment now...". Also, as someone who has a background in Laravel, are there Request Validation classes in Symfony? Regex in the middle of URL path seems very unreadable in my opinion.

1 Reply

Hey Filip,

> Good old rickroll, and that was loud as well! :D I'm not sure if it's been reported before or not, but there is an issue with pasting text into this input field, it adds empty rows. With each paste it gets progressively worse. Something to keep in mind.

I think I know what exactly you're talking about... if you're talking about Disqus comment field - yes, that's a known bug in Disqus, not sure why it's not fixed yet... but so far it's only problem for Firefox - the browser you're using I suppose? :) Try it in Chrome - no new lines should be added ;) Anyway, nothing much we can do with that unfortunately... though we're not happy with Disqus comments system too, so we're going to replace it with a custom one some day, but still no specific release date. Thanks for you patience and understanding!

Unfortunately, I'm not familiar with Laravel Request Validation... but I think you can find some answers in docs: https://symfony.com/doc/cur...

I hope this helps!

Cheers!

2 Reply

Yo Filip Jakovljevic!

And... to add some more:

> "Where are Models??

Are you referring to "database models"? If so, we do them (will do them) in the 3rd course of this series. I know you need them to build anything real, but we keep them "away" so we can focus on some "fundamentals". But yes, they're coming :).

> As someone who has a background in Laravel, are there Request Validation classes in Symfony? Regex in the middle of URL path seems very unreadable in my opinion

They're... sort of 2 different things. In this tutorial, we're talking about making a route simply NOT match unless the "wildcard" is a digit. It's not quite "validation". There IS a separate validation system - it's quite different than Laravel, but would feel much more familiar. Btw, this is the regex version that Laravel uses for what we did: https://laravel.com/docs/9.... - it's not in the route, but it's still regex.

And actually, on this last point, Symfony 6.1 has a nice feature for common regex requirements: some built-in regex via constants - https://symfony.com/blog/ne... (btw, that post also shows how you can put requirements as an option, instead of sticking them right INSIDE of the route string itself).

Cheers - we appreciate the comments and questions!

2 Reply
Filip J. Avatar

Thank you guys for replying. As for the Firefox bug, I am indeed on Firefox and it doesn't bother me at all. It just seemed like something that could easily slip under the cracks and I've wanted to report it in case it wasn't already, but since it was no worries about that. :)

Here's what I was referring to by Request Validation classes. Inside of this class there is a rules method where we can store all of our rules, i.e. required, numeric, and so on. There is also another method called failedValidation where you can customize the response for any scenario you'd like, in this case we could throw a 4xx and an appropriate JSON response. Yup, I am primarily talking from a standpoint of someone who is building a RESTful API so a 404 page wouldn't work for me, but even if I wasn't, that's how I would approach this. This method also avoids regex entirely, in or outside of the route. Is the validation system similar to this?

As for the Models, I'm referring to them as a part of the MVC structure. I guess that you guys call them Entities? The "database models" can be a bit misleading since they don't have to actually interact with the DB, as far as I know, although they're very useful when it comes to doing so.

To make things simpler, I am applying for a junior developer position. I have been given a task to do in Symfony and am looking at how to approach it. I need to make a small game/program where you can type a word and get points for it. The word needs to be an actual word, they get 1 point for each letter, 3 extra points if the word is a palindrome and 2 if it's almost a palindrome. I'd obviously have a view where user can type in their word, an API route towards the controller, and from the controller I'd make a Word object to validate the word. I understand the way views and controllers work, and while I could place the entire points system directly inside of a controller that's not the MVC way and that's pretty much how I've been taught to program. Would such logic belong inside of an Entity?

Sorry for the long reply, I hope that this could be useful to some other people in the future as well.

1 Reply

Hey Filip J. !

Sorry for the slow reply!

> Here's what I was referring to by Request Validation classes. Inside of this class there is a rules method where we can store all of our rules, i.e. required, numeric, and so on.

Ah, I see! Symfony doesn't have anything that's quite like this. Or, more accurately, this is done differently in Symfony :). Let me explain it in two parts:

A) First, the regex we used in the route - even in Laravel - I would STILL use route requirements for this. And I think I didn't really show this off correctly in the tutorial. This is because the MAIN purpose of that regex is not "validation" but to make it so that the route doesn't match... which is only really needed if you have 2 routes whose URLs are very similar. For example:


#[Route('/articles/{page}')]
public function allArticles(int $page)

#[Route('/articles/{slug}')]
public function showArticle(string $slug)

See the problem? :). Both match the same pattern! This is the main use-case for adding the regex: make page only match "digits"... and then if you go to /articles/foo that will NOT match the first route, but will fall down and match the second. So, not really validation, just a trick to make two very-similar routes match. I believe you would solve this in Laravel the same way.

However, with argument types (e.g. the int before the $page) argument, if you choose to use these, then these requirements have a second purpose (which is what I was showing in the tutorial): to make sure that going to /articles/foo does NOT match the first route... so that Symfony doesn't try to pass the string foo to the int $page argument. I suppose this is a type of validation, though mostly when we talk about validation, we're talking about validating a bunch of data (either POST or JSON data) that's sent on the request (which I think is the main purpose of those request validation classes in Laravel also).

B) Ok, so how DOES Symfony do validation, specifically for an API? You can do various levels of complexity, but we would typically do this:

i) Create a class (maybe it is a Doctrine entity class) with all of the fields in your API. For example, suppose we have an Article entity with some fields like title and body.

ii) Use Symfony's serializer to "deserialize" the incoming JSON into a new instance of Article. So now, that object is populated with the incoming data. It may, at this moment, be in an "invalid" state.

iii) Add validation constraints above the properties in your Article class, like #[Assert\NotBlank].

iv) Use Symfony's validator to validate the Article object itself. This will return a ConstraintViolationList (fancy object that holds an array of "violations").

v) Transform this into JSON. You could do this by hand, or you could once again use Symfony's serialize to serialize the ConstraintViolationList into JSON.

API Platform is a great platform that has a lot of this built-in. But ultimately, this is what it's doing behind the scenes :).

> Yup, I am primarily talking from a standpoint of someone who is building a RESTful API so a 404 page wouldn't work for me

Btw, if the user goes to /api/songs/foo, then you actually DO want to trigger a 404. But you're right, you don't want a 404 "page", but rather a 404 JSON response. If you're using my requirements trick, then Symfony will trigger the "404" for you before the controller is called. But you can "hint" to Symfony that this request is a "json" request, and that it should return the 404 error as JSON and not HTML. This is not as easy as it should be, I'll admit. You need to set this little _format thing - here's an example - https://stackoverflow.com/q... - if your entire site is an API, this is pretty easy to do - I'd add that defaults: { _format: 'json' } to the route import for the src/Controllers/ directory. You'll find this in config/routes/annotations.yaml or config/routes.yaml depending on your Symfony version.

The idea is kind of cool: you throw errors (like 404 errors) just like you would in an HTML application. Then Symfony returns an HTML or JSON version based on the "format" of the request. If you want to further customize what the 404 JSON looks like, you can: https://symfony.com/doc/cur...

> As for the Models, I'm referring to them as a part of the MVC structure. I guess that you guys call them Entities? The "database models" can be a bit misleading since they don't have to actually interact with the DB, as far as I know, although they're very useful when it comes to doing so.

Correct! We usually call these "entities"... but as far as MVC goes, you're right that "models" means something bigger. So, if you want to talk to the database, those are "Doctrine entities" and we'll get to those. Beyond that, "models" is often also used to refer to "services" (we'll create our own services in the next tutorial) and "simple data-hold objects that are not Doctrine entities", which you create ad-hoc in your app if/when you find yourself passing around some array of data.. which would be more organized in a class. So as usual, the "M" in MVC is where your business logic goes, so it's largely up to you to build what you need. In Symfony, we rely heavily on "services" in this M layer.

> The word needs to be an actual word, they get 1 point for each letter, 3 extra points if the word is a palindrome and 2 if it's almost a palindrome. I'd obviously have a view where user can type in their word, an API route towards the controller, and from the controller I'd make a Word object to validate the word. I understand the way views and controllers work, and while I could place the entire points system directly inside of a controller that's not the MVC way and that's pretty much how I've been taught to program. Would such logic belong inside of an Entity?

Ah, cool! I'd clarify if they want an API endpoint or not. This can also be built with Symfony's form component. Just two different approaches.

If you have time, I'd watch our "Symfony 5 Fundamentals" tutorial - specifically the parts about services. Services are classes that "do work". So, if you find that you have a bunch of "work" being done in a controller (like calculating a score), then that is a good candidate to pull out into a service - e.g. a `WordScorer` service, which has a public function where you can pass it the "submitted word" and it returns the integer score. In this case, you may or may not need a Word class... just because... what would it hold beyond the string "word" that was submitted? Put the scoring logic into a "service" - that's the "Symfony" way :).

Cheers!

Reply
Steve-D Avatar

Hi

I've just added and set up stimulus in one of my test applications. The speed increase is awesome however all my old jquery code stops working unless I hit refresh.

Do I need to move it all to a stimulus controller or is there a way to get it working?

Thank you

Steve

Reply

Hey Steve-D,

Ah, that might be tricky :) Ideally, rewrite all your JS code with Stimulus :p But if it requires a lot of time and you cannot do it right now - you can try to fix it. And it also depends if the problem in your custom JS code or in the JS code of a library you're using. The 2nd would be much harder to fix, and probably the best option is to switch to another similar JS library that can give you the same features but also works well with Stimulus.

But if the problem in your code - it might be easier to fix it. In theory, you just need to find elements in a different way stating finding them outside of the Stimulus code, .e.g if you replace some HTML with Stimulus, then outer HTML tag as a base jQuery tag, and then search required element from it, i.e. $('#out-of-stimulus-element').find('.something-inside-of-stimulus-element'). This way, no matter if the .something-inside-of-stimulus-element element will be replaced by Stimulus or not - you will always be able to find it this way.

Same way for any listeners you have. Actually, jQuery listeners already support this, all you need to do is to pass 3 arguments to the on() instead of 2, e.g.: $('body').on('click', '#out-of-stimulus-element', '.something-inside-of-stimulus-element'). Read more about it here: https://api.jquery.com/on/

I hope these tips help! But basically, yeah, there's no easy option you need to activate that will make things work for you - you need to debug your code yourself and find problematic places, then figure out why they do not work and fix them, i.e. rewrite them in a way that will work with Stimulus using some tricks I mentioned above.

Cheers!

Reply
Steve-D Avatar

Hi Victor

I suspected it might be a case of rewriting the code in Stimulus. I'm going to press on with the tutorial, I'm only at the beginning so need to understand more about Stimulus and Turbo before attempting anything but thank you so much for the detailed response.

Steve

Reply

Hey Steve-D,

Sure, yeah, give the tutorial a try till the end, I bet things will become clearer for you at the end.

Cheers!

Reply
Ruslan Avatar

Hi, How can I disable debug mode for stimulus?
In app.js I added :
import {app} from "./bootstrap";
app.debug = false;
OR
in bootstrap.js :
app.debug = false;

Is it correct way?

Thank you.

Reply

Hey Ruslan

I think that's the only way to deactivate it without running your app in production mode (env var: APP_ENV=prod)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    },
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3
    }
}