Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Actions: Listening to Events

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.

I want to make our controller more realistic: instead of being able to click anywhere on the element to increment the count, let's add a button.

Easy enough! In the template, add <button class="btn btn-primary btn-sm"> and then the excellent call to action: "Click me".

... lines 1 - 2
{% block body %}
... lines 4 - 11
<div data-controller="counter">
<button class="btn btn-primary btn-sm">
Click me!
</button>
<br>
... lines 18 - 21
</div>
... lines 23 - 95
{% endblock %}

If we check it in the browser... amazing! It works! Ok, I'm kidding: of course it works. But that's the cool thing about Stimulus: we get to do so much of our work in Twig where life is quick and easy.

Now: how could we attach a click listener to just this element? You might be thinking:

I know! Let's add a target to the button like we did for the span. Then we can find that button inside of connect() and add the event listener to it instead of this.element.

And yeah! That will totally do it! But it's way too much work.

Adding a Click Action

After targets, the second big feature of Stimulus is actions. Anytime you need to listen to an event, like click, submit, keyup or anything else, you can use an action instead.

Here's how it works: on the button element, I'll break this onto multiple lines for clarity. Now add data-action="click->counter#increment".

... lines 1 - 12
<button
data-action="click->counter#increment"
class="btn btn-primary btn-sm"
>
... lines 17 - 100

This is another special syntax: data-action="", then the name of the event, like click, submit or keyup, arrow, the name of the controller, a pound sign, and then the name of the method to call on our controller when this event happens. We'll create the increment method in a minute.

When I first used Stimulus, I did not love this syntax. It's... a bit weird. But it really does make life a lot nicer. And we'll simplify it a bit in a few minutes.

Over in the controller, add the method - increment() - then copy the logic from the click callback, delete, and paste it here.

And now we can remove the event listener entirely.

... lines 1 - 2
export default class extends Controller {
... lines 4 - 5
connect() {
this.count = 0;
}
increment() {
this.count++;
this.countTarget.innerText = this.count;
}
}

I may not love the data-action syntax in the template, but I do love the result. This is gorgeous.

Let's try it. Refresh the page. Now, if I click somewhere else in the element, nothing happens. If I click on the button, it increments! Woo!

The Default Action Name

But I did promise a simplification in the template. Remove click and the -> after it.

... lines 1 - 12
<button
data-action="counter#increment"
... line 15
>
... lines 17 - 100

Try it again: it's still works just fine! How? Stimulus has a default event name for the most common elements. If you add data-action to an a tag or a button, the default event name is click. If you add one to a <form> element, it defaults to submit. And if you add one to an <input> or <textarea>, it defaults to input, which is the event that happens when the value of the field changes.

So most of the time, you don't need to specify the event name.

Oh, and now, to celebrate, in the controller, we can remove the connect() method entirely! Move this.count = to a normal property: count = 0. Then delete connect().

... lines 1 - 2
export default class extends Controller {
count = 0;
... lines 5 - 10
}

Let's make sure I didn't break anything. Nope! All is good!

So... that is most of Stimulus... seriously! But I already love it. I mean, look how clean this controller is! And I get to render nice, clean HTML inside a template.

There are a bunch of things that I still want to talk about, like the values API, but Stimulus really is a lean and mean library.

Next: as nice as our counter example was, let's do something real. Over in the browser, click the "Furniture" category and then click the "Inflatable Sofa". Some products come in multiple colors and you choose the color with a color select element. Boooooooring. Let's enhance this by turning it into a color square selector widget.

Leave a comment!

7
Login or Register to join the conversation
Nick-F Avatar
Nick-F Avatar Nick-F | posted 1 year ago | edited

Even after removing the addEventListener from the connect() function, it still increments if I click on the text, not just the button.
This is my controller


    static targets = ['count'];

    connect() {
        this.count = 0;
    }

    increment() {
        this.count++;
        this.countTarget.innerHTML = this.count;
    }

This is my div:


<div data-controller="counter">
                    <button
                            data-action="click->counter#increment"
                            class="btn btn-primary btn-sm"
                    >
                        Click me
                    </button>
                    <br/>
                    I have been clicked
                    <span data-counter-target="count">0</span>
                    times!
                </div>

The listener is still attached to the data-controller div, not the button.
I'm in the dev environment, I cleared the cache, I have yarn watch running and it shows that it did compile successfully, and I also deleted the temporary files in my browser.

Reply
Nick-F Avatar
Nick-F Avatar Nick-F | Nick-F | posted 1 year ago | edited

I've even started and restarted the webpack encore server several times and tried deleting the increment method from the controller completely and still found this in the app.js build file:


  _createClass(_default, [{
    key: "connect",
    value: function connect() {
      var _this = this;

      this.count = 0;
      this.element.addEventListener('click', function () {
        _this.count++;
        _this.countTarget.innerHTML = _this.count;
      });
    }
  }]);

  return _default;
}(stimulus__WEBPACK_IMPORTED_MODULE_12__.Controller);

I even tried deleting all of the files in the public build folder and rebuilt them and the old controller code is still there in app.js
So for some reason, its not rebuilding the assets correctly

Reply

Yo Nick F. !

Hmm, that IS weird - and you've done some excellent debugging. I can't think of what could be going wrong :/. But I have two suggestions:

A) There is a cache inside of Webpack. However, it should not be causing this type of problem. But to be sure, you can delete the node_modules/.cache directory and rebuild to be sure.

B) In the built app.js file, do you see your NEW built code? Like, if you add a console.log('FOOOOOO') to your controller and rebuild, can you find this in your built app.js file?

C) If you delete your assets/app.js file, do you get a Webpack error? You should - this is just a sanity check to make sure that the most basic things are connected correctly.

Cheers!

Reply
Nick-F Avatar

Hey, the first time I literally had to delete the entire project and start over and that fixed it for a while, but it has happened again.
A. I have tried deleting the cache in the node_modules folder, but it didn't help.
B. I do see the new code inside of the built app.js file
C. I do get a webpack error if I delete the assets/app.js

All of the old controller logic still runs, but if I try to change anything or even make a brand new controller and put it onto an element, it's just completely ignored

Reply

Hey Nick F.!

Ah, this is crazy! It may seem obvious (so I apologize), but have you tried force-refreshing in your browser?

Also, you mentioned:

> B. I do see the new code inside of the built app.js file

This his helpful :). If you REMOVE some old code from the controller, is it REMOVED from this file, or still there? That might seem like an obvious question ("duh it is still there, the code is still executing!"). But... just double-check - both from in your browser AND directly from loading the public/build/app.js, like in your editor.

If we find out that it is NOT browser cache... them I'm not sure! I'm 99% sure that there is no other cache involved in the build process... and you DO see the new code so it is processing it.... so unless your browser is just being stubborn and using an outdated file, I'm super confused :p.

Let me know! Cheers!

Reply
Nick-F Avatar

Haha, I refreshed the browser, deleted all internet files, restarted my computer and still didn't work.
I know that new code appeared in the built app.js but I ended up finding a fix so I'm not sure if deleting old code caused that code to be removed from it.
The fix was: instead of running yarn watch or yarn encore dev, I ran the dev server with yarn encore dev-server and somehow it started to work.
Another possible clue for the cause was that both times it did this, I had left the symfony local server and yarn watch running for a couple hours while I was away.
I noticed that yarn encore dev-server does not create the built files in the build folder but links to them with absolute urls within the entrypoints and manifest json files

Reply

Hey Nick F.!

Haha, I refreshed the browser, deleted all internet files, restarted my computer and still didn't work

That's crazy!

The fix was: instead of running yarn watch or yarn encore dev, I ran the dev server with yarn encore dev-server and somehow it started to work.

This does serve the files in a different way... so it makes potential sense that this happened.

I noticed that yarn encore dev-server does not create the built files in the build folder but links to them with absolute urls within the entrypoints and manifest json files

With dev-server, your browser is actually fetching the files from a "live server" - not from physical files. So if something was getting "stuck"... or something :p, then this would, indeed make things more dynamic. About your clue:

and yarn watch running for a couple hours while I was away.

It's possible that the yarn watch process somehow just got stale. And so, when you made a change to a source file, it just wasn't rebuilding after a while (that would be easy to verify by modifying a file and seeing if the the terminal says it's rebuilding). But anyways, the dev-server is actually a cooler way to do things than "yarn watch" - it can even update CSS without full page reloads. I mostly don't use it in tutorials because if you have a more complex setup, it can be harder to get working (so I don't want to bring all that setup baggage into the tutorial).

Cheers!

Reply
Cat in space

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

This tutorial works perfectly with Stimulus 3!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "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.18.5
        "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.2.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.30.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
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "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-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-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
    }
}
userVoice