Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Real-World Stimulus Example

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.

Let's put Stimulus to the test. Here's our goal: when we click the play icon, we're going to make an Ajax request to our API endpoint... the one in SongController. This returns the URL to where this song can be played. We'll then use that in JavaScript to... play the song!

Take hello_controller.js and rename it to, how about song-controls_controller.js. Inside, just to see if this is working, in connect(), log a message. The connect() method is called whenever Stimulus sees a new matching element on the page.

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!');
}
}

Now, over in the template, hello isn't going to work anymore, so remove that. What I want to do is surround each song row with this controller.... so that's this song-list element. After the class, add {{ 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 %}

Let's try that! Refresh, check the console and... yes! It hit our code six times! Once for each of these elements. And each element gets its own, separate controller instance.

Adding Stimulus Actions

Okay, next, when we click play, we want to run some code. To do that, we can add an action. It looks like this: on the a tag, add {{ stimulus_action() }} - another shortcut function - and pass this the controller name that you're attaching the action to - song-controls - and then a method inside of that controller that should be called when someone clicks this element. How about 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 %}

Cool huh? Back in song controller, we don't need the connect() method anymore: we don't need to do anything each time we notice another song-list row. But we do need a play() method.

And like with normal event listeners, this will receive an event object... and then we can say event.preventDefault() so that our browser doesn't try to follow the link click. To test, console.log('Playing!').

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

Let's go see what happens! Refresh and... click. It's working. It's that easy to hook up an event listener in Stimulus. Oh, and if you inspect this element... that stimulus_action() function is just a shortcut to add a special data-action attribute that Stimulus understands.

Installing and Importing Axios

Ok, how can we make an Ajax call from inside of the play() method? Well, we could use the built-in fetch() function from JavaScript. But instead, I'm going to install a third-party library called Axios. At your terminal, install it by saying:

yarn add axios --dev

We now know what this does: it downloads this package into our node_modules directory, and adds this line to our package.json file.

Oh, and side note: you absolutely can use jQuery inside of Stimulus. I won't do it, but it works great - and you can install - and import - jQuery like any other package. We talk about that in our Stimulus tutorial.

Ok, so how do we use the axios library? By importing it!

At the top of this file, we're already importing the Controller base class from stimulus. Now import axios from 'axios'. As soon as we do that, Webpack Encore will grab the axios source code and include it in our built JavaScript files.

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

Now, down here, we can say axios.get() to make a GET request. But... what should we pass for the URL? It needs to be something like /api/songs/5... but how do we know what the "id" is for this row?

Stimulus Values

One of the coolest things about Stimulus is that it allows you to pass values from Twig into your Stimulus controller. To do that, declare which values you want to allow to passed in via a special static property: static values = {}. Inside, let's allow an infoUrl value to be passed. I totally just made up that name: I'm thinking we'll pass in the full URL to the API endpoint. Set this to the type that this will be. So, a String.

We'll learn how we pass this value from Twig into our controller in a minute. But because we have this, below, we can reference the value by saying 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()
}
}

So how do we pass that in? Back in homepage.html.twig, add a second argument to stimulus_controller(). This is an array of the values you want to pass into the controller. Pass infoUrl set to the URL.

Hmm, but we need to generate that URL. Does that route have a name yet? Nope! Add 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
}
}

Perfect. Copy that... and back in the template, set infoURl to path(), the name of the route... and then an array with any wildcards. Our route has an id wildcard.

In a real app, these tracks would probably each have a database id that we could pass. We don't have that yet... so to, kind of, fake this, I'm going to use loop.index. This is a magic Twig variable: if you're inside of a Twig for loop, you can access the index - like 1, 2, 3, 4 - by using loop.index. So we're going to use this as a fake ID. Oh, and don't forget to say id: then loop.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 %}

Testing time! Refresh. The first thing I want you to see is that, when we pass infoUrl as the second argument to stimulus_controller, all that really does is output a very special data attribute that Stimulus knows how to read. That's how you pass a value into a controller.

Click one of the play links and... got it. Every controller object is passed its correct URL!

Making the Ajax Call

Let's celebrate by making the Ajax call! Do it with axios.get(this.infoUrlValue) - yes, I just typo'ed that, .then() and a callback using an arrow function that will receive a response argument. This will be called when the Ajax call finishes. Log the response to start. Oh, and fix to use 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);
});
}
}

Alrighty, refresh... then click a play link! Yes! It dumped the response... and one of its keys is data... which contains the url!

Time for our victory lap! Back in the function, we can play that audio by creating a new Audio object - this is just a normal JavaScript object - passing it response.data.url... and then calling play() on this.

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();
});
}
}

And now... when we hit play... finally! Music to my ears.

If you want to learn more about Stimulus - this was a bit fast - we have an entire tutorial about it... and it's great.

To finish off this tutorial, let's install one more JavaScript library. This one will instantly make our app feel like a single page app. That's next.

Leave a comment!

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
    }
}