Stimulus Controllers

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.

Ok: time for Stimulus! First stimulus is... a JavaScript library! If you start a new project and install Encore fresh, like we did, then thanks to the recipe, stimulus is already inside of your package.json file.

We also have an @symfony/stimulus-bridge library. This adds superpowers on top of stimulus. I'll tell you exactly what those are as we go along.

If you don't have these packages, install them with:

yarn add stimulus @symfony/stimulus-bridge --dev

Let me close a few tabs and open up our assets/app.js file. This imports a bootstrap.js file that the recipe also gave us. And you will need this: if you don't have it, you can get it from the code block on this page or the stimulus-bridge docs.

The one line in this file starts stimulus by telling it to look for stimulus controllers in this controllers/ directory, which is literally assets/controllers/. Symfony gives us one dummy controller to start.

And... yea, the entire point of this file is to say:

Hey Stimulus! I have some controllers in this controllers/ directory.

We'll learn what all this weird lazy-controller stuff is a bit later, it's not important yet.

Creating our First Controller

The best way to see how Stimulus works is just to try it. Delete the hello_controller.js file: let's create our first controller from scratch. Call it counter_controller.js. To learn the Stimulus basics, we're going to create an element that tracks how many times we click it.

Oh and this naming convention is important. All of our controller files will be something_controller.js. And you'll see why in a minute.

Inside the file, these always start the same way: import {} from stimulus and what we want to import is Controller. Then, export default a class that extends Controller. Inside the class, add a method called connect() with this.element.innerHTML = a message:

You have clicked zero times ­čśó

import { Controller } from 'stimulus';
export default class extends Controller {
connect() {
this.element.innerHTML = 'You have clicked me 0 times ­čśó';
}
}

Adding the data-controller Element

That's all we need for now. To see what this does, we need to add a matching element to one of our pages. Open templates/product/index.html.twig.

This is the template for the homepage. Down a bit, how about at the top of the main content, add <div data-controller="counter"></div>.

... lines 1 - 2
{% block body %}
... lines 4 - 9
<div class="col-xs-12 col-9">
... line 11
<div data-controller="counter"></div>
... lines 13 - 82
</div>
... lines 84 - 85
{% endblock %}

We can put something in the div, but we don't need to for our example.

The data-controller connects this element to the controller class that we just created. Because we named the file counter_controller.js, the controller's name internally in Stimulus is counter: it strips off the _controller.js part.

So we connected the element to that controller with data-controller="counter". Thanks to that, this should work!

As a reminder, I still have a yarn watch going over in my terminal, so it's been happily rebuilding each time we make a change.

Spin over to your browser and click to get to the homepage. Yes! It's alive! The empty div has our message! Inspect that element. Yep! We can see data-controller and the text inside.

Elements & Controller Objects

This is the magic of stimulus. As soon as it sees an element with data-controller="counter", it instantiates an instance of our "counter" controller and calls the connect() method... named that way because Stimulus is "connecting" this object to a specific element on the page. And, as you can see, the element we just got connected to is available via this.element.

That allowed us to easily set its inner HTML.

Multiple Controller Instances on the Page

The beauty is that we can have as many of these elements on the page at the same time as we want. I'll copy the div and, up in the aside, paste.

... lines 1 - 2
{% block body %}
... lines 4 - 5
<aside class="col-xs-12 col-3">
<div data-controller="counter"></div>
... line 8
</aside>
... lines 11 - 86
{% endblock %}

Go refresh now. Two messages! And the really cool part is that each element is connected to a separate instance of our controller class. This means we get to write clean JavaScript code in a class and store information specific to its element as properties on that object. We'll do that very soon.

So... with Stimulus, we get objects that are bound to individual HTML elements and are instantiated automatically when those elements appear on the page. I would use Stimulus just for that! It's the simple, object-oriented JavaScript approach I've always tried to create on my own.

But wait there's more! Next: let's add a count property and a click listener to show how each element is connected to a separate controller object. Then I'll show you the feature of Stimulus that absolutely knocked me over when I first saw it.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.12.1
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.1.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.28.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@symfony/stimulus-bridge": "^2.0.0", // 2.0.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^4.6.0", // 4.6.0
        "core-js": "^3.0.0", // 3.8.3
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-use": "^0.22.1", // 0.22.1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}