Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Stimulus Behaviors: stimulus-use

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We have a little itty bitty problem. When we click off of our search area, the suggestion box... just sticks there. We need that to close. How can we do that?

We could do it manually. We would register a click listener on the entire page document and then detect if a click was inside our search area or outside of it.

Hello stimulus-use

But... I have a way better solution. Search for "stimulus use" to find a GitHub page. stimulus-use is a library that's packed full of behaviors for Stimulus controllers. It's awesome.

I'll click down here to get into the documentation.

Here's a great example of the power of this library. Suppose you want to do something whenever an element appears in the viewport - like as the user is scrolling - or disappears from the viewport. You can easily do that with one of the behaviors called useIntersection.

Basically, you activate it, give it some options if you want... and boom! The library will automatically call an appear() method on your controller when your element enters the viewport and disappear() when it leaves the viewport. How cool is that?

One of the other behaviors - useClickOutside - is exactly what we need.

Installing & Activating useClickOutside

So let's get this installed. Over on "Usage"... actually "Getting Started", the name of the library is stimulus-use. Spin over to your terminal and install it:

yarn add stimulus-use --dev

Again, the --dev part isn't really important - that's just how I like to install things.

While that's working, let's go look at the documentation for useClickOutside. I'll scroll down to "usage".

Ok: step 1 is to activate the behavior in our connect() method. Cool. Copy this line... and let's make sure the library finished downloading. It did.

Over in the controller, go to the top to import the behavior: import {} and then the behavior we need - useClickOutside.

Sweet! PhpStorm auto-completed the rest of that line for me.

Below, add a connect() method and paste: useClickOutside(this).

... line 1
import { useClickOutside } from 'stimulus-use';
... line 3
export default class extends Controller {
... lines 5 - 10
connect() {
useClickOutside(this);
}
... lines 14 - 27
}

For step 2, look at the docs: we need to add a clickOutside() method. Ok! Let's add it at the bottom: clickOutside(event). When the user clicks outside of our controller element, we will set this.resultTarget.innerHTML = ''.

... lines 1 - 3
export default class extends Controller {
... lines 5 - 24
clickOutside(event) {
this.resultTarget.innerHTML = '';
}
}

Done. Let's test it! Head back to the browser and refresh. Type a little to get some suggestions, then click off. Beautiful! And if I type again... it's back, then click off... and gone again.

People: that was like four lines of code!

Debouncing with useDebounce

Since that was so fast, let's do something else.

If I type really, really fast - watch the Ajax counter right here - yup! We make an Ajax call for every single letter no matter how fast we type. That's overkill. The fix for this is to wait for the user to pause for a moment - maybe for 200 milliseconds - before making an Ajax call. That's called debouncing. And there's a behavior for that: useDebounce.

Let's try it! Scroll up to the example. Of course, we need start by importing it. Oh, and this ApplicationController thing? Don't worry about that: that's another, optional feature of this library, they're just mixing examples.

Over in the controller, at the top, import useDebounce. Next... if you look at the other example, we activate it the same way. So, in connect(), useDebounce(this). I'll add semi-colons... but they're obviously not needed.

... line 1
import { useClickOutside, useDebounce } from 'stimulus-use';
... line 3
export default class extends Controller {
... lines 5 - 11
connect() {
... line 13
useDebounce(this);
}
... lines 16 - 29
}

Here's how this behavior works: we add a static debounces property set to an array of methods that should not be called until a slight pause. That pause is 200 milliseconds by default.

For us, we want to debounce the onSearchInput method. Copy the name then head up to the top of the controller: static debounces = [] with onSearchInput inside.

... lines 1 - 3
export default class extends Controller {
... lines 5 - 9
static debounces = ['onSearchInput'];
... lines 11 - 29
}

Let's try it! Back to the browser, refresh and... type real fast! Ah! It exploded! This is due to a limitation of this feature. Because our browser is calling onSearchInput, the behavior can't hook into it properly. Debouncing only works for methods that we call ourselves.

But that's no problem! We just need to organize things a bit better. Try this: close up onSearchInput early and move most of the logic into a new method called async search() with a query argument.

Again, we're making this async because we have an await inside.

For onSearchInput, we don't need the async anymore... and we can now call this.search() and pass it event.currentTarget.value.

... lines 1 - 3
export default class extends Controller {
... lines 5 - 16
onSearchInput(event) {
this.search(event.currentTarget.value);
}
... line 20
async search(query) {
... lines 22 - 28
}
... lines 30 - 33
}

Below, set the q value to query.

... lines 1 - 20
async search(query) {
const params = new URLSearchParams({
q: query,
... line 24
});
... lines 26 - 28
}
... lines 30 - 35

This is good: we've refactored our code to have a nice, reusable search() method. And now we can change the debounce from onSearchInput to search.

... lines 1 - 3
export default class extends Controller {
... lines 5 - 9
static debounces = ['search'];
... lines 11 - 33
}

Testing time! Refresh and... type real fast. Yes! Only one Ajax call.

Alright! This feature is done! Next, on the checkout page, let's add a confirmation modal when the user removes an item from the cart. For this, we'll leverage a great third party library from inside our controller: SweetAlert.

Leave a comment!

15
Login or Register to join the conversation
Peter L. Avatar
Peter L. Avatar Peter L. | posted 4 months ago

Personally when I am typing something to search box I really do not like mouse-only behaviour.
Was looking how to add 'ESC' key behaviour to this modal and also key-down/enter one.
ESC key listening:
https://discuss.hotwired.de...
If link is not working I am writing down kaspermeyer's answer:

Why not make it a Stimulus action instead, though? If you define your modal somewhere along these lines:


<div data-controller="modal" data-action="keydown@window-&gt;modal#close">

</div>

You could simplify your controller and you won’t have to manage event listeners manually:

import { Controller } from 'stimulus'

export default class extends Controller {
static targets = [ "wrapper" ]

close(event) {
if (event.keyCode == 27) {
this.wrapperTarget.style.display = 'none'
}
}
}
1 Reply

hey Peter Labos

That is pretty good example! Thx for sharing it!

Cheers!

Reply
Intexsys I. Avatar
Intexsys I. Avatar Intexsys I. | posted 7 months ago

For stimulust v3 usage check BC notice -- https://github.com/stimulus...

1 Reply

Hey Kirill,

Thank you for sharing a link to BC notes in V3 of Stimulus, it might be useful for others.

Cheers!

1 Reply
Tristan P. Avatar

using the beta version hepled, but it is not possible to use useDebounce, because of this bug:

https://github.com/stimulus...

any workarounds for that?

Thanks

1 Reply
Tristan P. Avatar

Ok, I just had to think about it for a second myself. The workaround is easy and quite obvious.

Instead of:


const params = new URLSearchParams({
speaker: event.currentTarget.value,
preview: 1,
});

Is just use:


const params = new URLSearchParams({
speaker: this.inputTarget.value,
preview: 1,
});

and add the data-search-preview-target="result" attribute to the input-field and add input to the static targets so there is no need to use currentTarget anymore.

1 Reply

Hey Tristano,

So you basically need to replace "event.currentTarget" with "this.inputTarget". Thank you for sharing this solution with others!

Cheers!

Reply
Ruslan Avatar

Hi.
I've got 15 errors like:
Module not found:
"./node_modules/stimulus-use/dist/use-application/application-controller.js" contains a reference to the file "stimulus"

As I understand it's happens because I use "@hotwired/stimulus": "^3.0.0",
And when I install stimulus-use I see
warning " > stimulus-use@0.41.0" has unmet peer dependency "stimulus@>=1.1.1 <3"

How to fix it? Should I downgrade to "stimulus": "^2.0.0" ?

Thank you.

1 Reply

Hey Ruslan!

Ah, sorry about the troubles! The stimulus-use library has support for Stimulus v3, however they haven't created a tag yet, and I'm not sure why :/. Try running yarn add stimulus-use@beta --dev to get their beta version, which should support it.

Cheers!

Reply
Default user avatar

I had the same problem. But I kind of "complicated" my setting using yarn workspaces (multiple package.json's and multiple webpack configs, etc). But I narrowed it down to stimulus-use (for useDispatch).
Ryan's solution worked for me:

"stimulus-use": "^0.50.0-2",

1 Reply
Ruslan Avatar

Hi ,
Thank you. It helps. After yarn add stimulus-use@beta --dev
We can see "stimulus-use": "^0.50.0-2" in package.json

Reply
El hadji babacar S. Avatar
El hadji babacar S. Avatar El hadji babacar S. | posted 1 year ago

His everyone, I have encountered problems with stimulus-use. Webpack reports this error to me:

Module build failed: Module not found:
"./node_modules/stimulus-use/dist/use-hotkeys/use-hotkeys.js" contains a reference to the file "hotkeys-js".
This file can not be found, please check it for typos or update it if the file got moved.

If anyone could help me please I searched the web. But I did not find any solution.
Thanks

Reply
El hadji babacar S. Avatar
El hadji babacar S. Avatar El hadji babacar S. | El hadji babacar S. | posted 1 year ago

I just found the solution, it requires installing hotkeys.js. Hope this helps those who are going to encounter this problem. Thanks for your great tutorials !!!!

1 Reply
Kai Avatar

When i try to import useClickOutside from stimulus-use, i get the following error from in webpack watch:

Module build failed: Module not found:
"../node_modules/stimulus-use/dist/use-application/application-controller.js" contains a reference to the file "stimulus".
This file can not be found, please check it for typos or update it if the file got moved.

stimulus_controller.js:

import { Controller } from 'stimulus';
import { useClickOutside } from 'stimulus-use'import

Any Ideas? (Im not using the Tutorial files. I try to use stimulus in my own app)

Reply
Kai Avatar

Never mind. I got it. Because i use ddev i installed stimulus-use in the wrong directories.
Thanks for your great tutorials!

1 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": ">=7.4.0",
        "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
    }
}