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!

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