Autocomplete with Transitions

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

The great thing about the third party stimulus-autocomplete controller is that we were able to quickly create a nice search auto-complete feature with full control over the HTML. And this required zero custom JavaScript.

The only bummer is that we lost our fade-in, fade-out transition! How can we add that back?

When we originally added the transition, we did it by leveraging useTransition() from the stimulus-use library. We added the behavior to the connect() method of a custom controller... then called this.enter() and this.leave() to show and hide the element... with a transition.

But... we can't exactly go into the stimulus-autocomplete source code and hack that stuff in! But we can make this work... as long as this 3rd party controller dispatches the right events.

If you look at their docs... and scroll down: they have an "Events" section! And they do trigger a bunch of events! This toggle one looks perfect:

Fires when the results element is shown or hidden.

We can use that to trigger our transition.

Using my stimulus-autocomplete Fork

Before we jump in, to get this to work, we need a pull request to be merged into this library, which... at the time of this recording... is still waiting to be merged! Rats! Since it's unmerged, we'll use a fork of this library that contains the tweaks from this pull request. Woo! Living on the edge!

How can we use a fork? Open your package.json file and find the stimulus-autocomplete line. Set its version to a specific branch on my repository: I'll paste that in. This is my username, the name of the library... and then it points to a branch I created called toggle-event-always-dist.

30 lines package.json
{
"devDependencies": {
... lines 3 - 14
"stimulus-autocomplete": "https://github.com/weaverryan/stimulus-autocomplete#toggle-event-always-dist",
... lines 16 - 19
},
... lines 21 - 28
}

To download this new version, find your terminal and run:

yarn install

Creating & Using a 2nd Controller

While that downloads, let's discuss the plan. In order to leverage the useTransition behavior, we're going to need our own custom controller. In assets/controllers/, create a new file called, how about, autocomplete-transition_controller.js. I'll go steal some code from another controller: the only thing we need right now is a connect() method... with console.log('i want transitions').

import { Controller } from 'stimulus';
export default class extends Controller {
connect() {
console.log('I want transitions!');
}
}

Back in the template for this page - which lives at templates/product/index.html.twig - we're now going to add two controllers to the same element. We can do that with the stimulus_controller() function... we just need to tweak the format a bit.

The first argument becomes an object... and each controller becomes a key in the object assigned to its values. Once I finish rearranging things... yup: we're passing an object with one controller set to the values for that controller. Now we can add our new controller name - autocomplete-transition - and... this doesn't need any values, so set it to an empty object.

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
<div
class="input-group"
{{ stimulus_controller({
'autocomplete': {
url: path('app_homepage', { preview: 1 })
},
'autocomplete-transition': {}
}) }}
>
... lines 48 - 60
</div>
</form>
... lines 63 - 112
{% endblock %}

Let's see if the new controller is connected! Find our site... I'll open my dev tools, refresh the page, check the console and... got it! There's our log. If you inspect element on the text box... you can see that the data-controller attribute now has both controllers on it.

Adding an Action for "toggle"

Ok, let's think: we need our new controller to be notified whenever the autocomplete results should be shown or hidden... so that we can trigger the enter() or leave() transition.

This is where that toggle event comes in handy. The stimulus-autocomplete controller dispatches all of its events on the "main element" that's registered for the controller, which means it's going to be on this div. Let's add an action for the toggle event. Do that with data-action="", the name of the event we want to listen to - toggle - an ->, the name of the controller to call - that's our new custom controller autocomplete-transition - a # sign and the method to call on that controller: let's use toggle.

... lines 1 - 37
<form>
<div
... lines 40 - 46
data-action="toggle->autocomplete-transition#toggle"
>
... lines 49 - 61
</div>
</form>
... lines 64 - 115

Copy that and head over to our controller. Add the new toggle() method with an event argument... and console.log(event).

... lines 1 - 2
export default class extends Controller {
... lines 4 - 7
toggle(event) {
console.log(event);
}
}

With any luck, whenever the autocomplete results are shown or hidden, we should see this line get hit. Let's see if that happens! Refresh the page. I'll go back to the console and... yes! Actually an event was dispatched immediately when the component initialized. This second one is from when the element was shown. Open up the event object and look at the detail property. Ah: it has a sub-key call action set to the string open! When the results close - like when we click off the area... we see another event. This time action is set to close.

In the toggle() method, we can use this info to either call this.enter() to fade in the element or this.leave() to fade it out.

But in order to even have those methods, we need to initialize the useTransition behavior on this controller.

Let's do that next... but in a way that will allow us to easily reuse our transitions in other controllers. Code-reuse: booya!

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