Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Using the autocomplete-controller

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

Now that we have a new autocomplete controller registered, let's try to use it instead of our custom search-preview controller to power the search preview functionality.

Setting up the Controller in HTML

To see how, go back to the docs. Hmm... looking at this example, we need to add the data-controller attribute to an element that's around both the input that we type into and the div where the results will go. We also need to pass a url value set to where the Ajax call should be made. The input needs a target called input... and the results go into a target called results. We don't need to worry about this hidden element: that's only needed if you're building a form and want to update a form field when the user selects an option.

Open the template for the homepage: templates/product/index.html.twig. First, change the name of the controller from search-preview to autocomplete. Then... nice! We were already passing a value called url, so that's done.

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
<div
... line 40
{{ stimulus_controller('autocomplete', {
... line 42
}) }}
>
... lines 45 - 57
</div>
</form>
... lines 60 - 109
{% endblock %}

Next, on the input, we don't need the "action" anymore: the controller will handle setting that up for us. But we do need to identity this as the text input by adding a target: data-autocomplete-target="input".

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
... lines 39 - 44
<input
... lines 46 - 50
data-autocomplete-target="input"
>
... lines 53 - 58
</form>
... lines 60 - 109
{% endblock %}

Finally, update the "results" target to use the new controller name - autocomplete - and the target name is now results with an "S".

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
... lines 39 - 53
<div
... line 55
data-autocomplete-target="results"
></div>
... line 58
</form>
... lines 60 - 109
{% endblock %}

Done. Let's try it! Move over, find our site, refresh, type "di" and... nothing happened! Well, not nothing. I can see that there was an Ajax call. In fact, two Ajax calls: I can see that down in the web debug toolbar.

Let's check out one of these in our Network tool. Look at the "Preview". Whoa! It's a little small... but this is the full HTML page. Let's make this bigger. Yep, this didn't render just the results partial, it returned the full page!

What happened?

How the autocomplete Ajax Works

Okay. I noticed a few things. To start, by complete chance, the autocomplete controller sends the contents of our input as a query parameter called q... which is exactly what we were using before! You can see that in src/Controller/ProductController.php: we read q as our search term. Awesome!

... lines 1 - 14
class ProductController extends AbstractController
{
... lines 17 - 20
public function index(Request $request, CategoryRepository $categoryRepository, ProductRepository $productRepository, Category $category = null): Response
{
$searchTerm = $request->query->get('q');
... lines 24 - 40
}
... lines 42 - 58
}

But we also look for a ?preview query parameter to know if we should render a page partial.

... lines 1 - 20
public function index(Request $request, CategoryRepository $categoryRepository, ProductRepository $productRepository, Category $category = null): Response
{
... lines 23 - 28
if ($request->query->get('preview')) {
return $this->render('product/_searchPreview.html.twig', [
... line 31
]);
}
... lines 34 - 40
}
... lines 42 - 60

Previously, in search-preview controller, we added that query parameter manually in JavaScript. We can't do that now... but that's ok! We can add it to the url value.

In index.html.twig, back up on the url, add a second argument to path and pass preview: 1. That will fix the full page problem.

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
<div
... line 40
{{ stimulus_controller('autocomplete', {
url: path('app_homepage', { preview: 1 })
}) }}
>
... lines 45 - 57
</div>
</form>
... lines 60 - 109
{% endblock %}

But if you try it now... the same thing happened again! An Ajax call was made... but no results are showing up! On the network tab... yeah! It is now returning the partial, not the full page. So... why don't we actually see the results below the input?

Adding the role="option" Attribute

Because... the other rule of this library - which I totally would have noticed if I had read the documentation a bit more carefully (sorry!) - is that each result must be identified by a role="option" attribute. But we don't need this data-autocomplete-value attribute: that controls the value that would go into the hidden input... which we don't need. But we definitely need the role="option" thing.

Let's go add it! The template for the partial lives at templates/product/_searchPreview.html.twig. On the <a> tag, which represents a single option, add role="option".

<div class="list-group">
{% for product in products %}
<a
role="option"
... lines 5 - 6
>
... lines 8 - 18
</a>
... lines 20 - 21
{% endfor %}
</div>

Oh, and down here on the "no results", we need to do the same thing: role="option". And if you look again at the documentation, to have an option but make it not selectable, you can add aria-disabled="true". That will make it show up on the list... but I won't be able to select it.

... line 1
{% for product in products %}
... lines 3 - 19
{% else %}
<div class="list-group-item" role="option" aria-disabled="true">No results found!</div>
{% endfor %}
... lines 23 - 24

This time, back on the site, we don't even need to refresh the page: I can type and boom! There it is! It looks exactly like before! And as a bonus, the controller has a feature that ours never did: the ability to hit up and down on our keyboard to go through the results. And, pressing enter, activates that item.

Oh, and by the way! Technically, we did not need to make our controller for the Ajax call return a partial. If we returned the whole page from the Ajax call... it would still work exactly like it does now... because the autocomplete controller looks for the role="option" attribute and only renders those. Rendering just the partial is a bit faster... but it's technically not needed... which is kind of amazing!

Let's celebrate by deleting our old search-preview controller. Thanks for teaching us how to use Stimulus! But now it's time for us move forward with less custom code.

Making third-Party Controllers Lazy

I have one more wish... or question: could we make this autocomplete controller load lazily? Earlier we made the whole chart-js controller "lazy" in controllers.json by setting fetch: 'lazy'. We also made our own submit-confirm_controller lazy by adding this special comment above the class.

But what about a third-party controller? We don't register this in controllers.json and... we can't exactly open up the file and add a comment in it. So, can we make it lazy?

We can! Though, due to some rigidness in how Webpack works, the syntax... isn't amazing. In bootstrap.js, we need to change the import to pass through the special @symfony/stimulus-bridge/lazy-controller-loader. You can do that by literally saying import { Autocomplete } from... - the name of that loader - an exclamation point, and then the name of the module that you want to import.

So, this module will now be passed through the loader. That... by itself, won't change anything. But now, before the exclamation point, add ?lazy=true.

The lazy-controller-loader is what looks for the stimulusFetch comment above the controller. Since that won't be there, this ?lazy=true is our way to force laziness.

And, normally, this would be all we need! It's a bit ugly, but not too bad! And we get the laziness we want.

However, notice that the stimulus-autocomplete module uses a named export instead of a default export. What I mean is, when we use this, we have to say import { Autocomplete } - that's a named export - instead of just being able to say import Autocomplete. If we tried this, it would not work.

Anyways, since the library uses a named export, we need to notify the loader about this so it knows where to find the code. We do that by adding one more loader option: &export=Autocomplete: the name of the import.

... line 1
import { Autocomplete } from '@symfony/stimulus-bridge/lazy-controller-loader?lazy=true&export=Autocomplete!stimulus-autocomplete';
... lines 3 - 13

Ok, now we're done. It's ugly, but it gets the job done if you need a 3rd party controller to load lazily.

Let's see it in action. Move over, refresh the page... and go down to your Network tools filtered for JavaScript. Yes! Look at this long name: this is the file that contains stimulus-autocomplete. The "Initiator" proves that it's being downloaded lazily, only after the data-controller="autocomplete" was found on the page.

If we go to any other page... that code is never downloaded.

The new stimulus-autocomplete controller allows us to have less custom code and better functionality, with the ability to press up and down on the search results. But we did lose one thing: the nice CSS transitions! Can we somehow add those to this third-party controller? As long as the controller dispatches the right events, we totally can. Let's learn how next

Leave a comment!

20
Login or Register to join the conversation
Peter L. Avatar
Peter L. Avatar Peter L. | posted 1 year ago

For the version 3 of this autocomplete controller thety must repaired something as results are visible even without role="option". But they are not accessible by keyboard without that.

Reply

Hey Peter L.!

Ah, thanks for the tip! So, if I understand it, you *should* still have the role="option"... but it's not quite as catastrophic if you forget it :).

Cheers!

Reply
chessserver Avatar
chessserver Avatar chessserver | posted 1 year ago

I am currently using a javascript based autocomplete function, based on the description from "Symfony 4 Forms: Build, Render & Conquer!" . It is used not only as a single item form, but also to select several users in a single form. Creating a SearchUserType was very helpful, including the rendering in Javascript etc. How do I create a similar FormType with the stimulus-controller?

Reply

Hey chessserver!

Excellent question! The PHP side of things would basically be the same... you would mainly just be "converting" the JavaScript from that tutorial into a Stimulus controller, which should feel very nice actually :).

Here are a few pointers:

A) Instead of adding a js-user-autocomplete class to the form element, you would add a data-controller="autocomplete" attribute (assuming you call your controller "autocomplete": https://symfonycasts.com/screencast/symfony-forms/autocomplete-js#codeblock-d8e290f178

B) Similar to the above, you would pass the URL in as a "value" to the controller. So, assuming your controller name is "autocomplete" and you create a value called url, you would set a data-autocomplete-url-value.

With (A) and (B), when your field renders, it will contain the needed attributes to activate an "autocomplete" Stimulus controller and pass the url into it as a "url" value. The last step would be to:

C) Convert the JavaScript into a Stimulus controller. It would look something like this:


// assets/controllers/slideshow-controller.js

import { Controller } from "@hotwired/stimulus"
import $ from 'jquery';

export default class extends Controller {
    static values = { url: String }

    connect() {
        const url = this.urlValue;

        $(this.element).autocomplete({hint: false}, [
            {
                source: function(query, cb) {
                    $.ajax({
                        url: url+'?query='+query
                    }).then(function(data) {
                        cb(data.users);
                    });
                },
                displayKey: 'email',
                debounce: 500
            }
        ])
    }
}

That should be it :). This uses connect to initialize the autocomplete functionality. We reference the input via this.element... and we grab the URL from this.urlValue. This, like all things with Stimulus, has the advantage over the code in that tutorial that even if you loaded a form via Ajax, this behavior would correctly be initialized.

Let me know if this helps!

Cheers!

Reply
chessserver Avatar

@Ryan, thanks for your answer! This works with the original setup. However, I have been a bit unspecific. I would like to replace the above autocomplete mechanism with the stimulus-autocomplete controller introduced in this lesson.

I am struggling to setup the div_layout for the new widget correctly. The base structure should be similar to the from in product/index.html.twig. But how to transfer the url and the searchTerm correctly? Is a wrapper around the presented controller required, replacing the .autocomplete in your example above? How would the whole block starting with $(this.element) look like?

I want to implement some other 3rd-party stimulus-controllers as well, so this would help me to progress. Unfortunately, my JS Knowledge is limited.

Reply

Hey chessserver!

Apologies for my slow reply! Ok, let me give you some pointers (which are hopefully helpful!) and then you can tell me what else might be unclear (I usually reply faster than this time!).

I am struggling to setup the div_layout for the new widget correctly

Ok, on a high level, here is how this will probably work:

A) In your form field template, you will need one element that is around everything - e.g. a div - and this will have {{ stimulus_controller() }} rendered onto it.

B) How to pass the URL? You can pass the "url" to stimulus_controller as a value (i.e. the 2nd argument to the controller). You could hardcode this - e.g. url: path('some_route') or you could pass the URL into the template as a Twig variable. You would do this by (A) creating a custom "field option" on your new field - e.g. url - and then (B) in buildView() of your custom field, you read that option and set a custom "variable" called "url". We do something similar here: https://symfonycasts.com/screencast/symfony-forms/build-view#codeblock-8c63ee1cf8. You could read the "url" from $options and then create a new $view->vars['url'] = $options['url'];. Then, in 2nd arg of stimulus_controller, you would have { url: url }

C) With this setup, your stimulus_controller is on an element around the input, but not actually on the input. That's ok! You could set a "target" on the input - using stimulus_target - and then use that in the controller. Or, as a shortcut, because you will only have one input element inside your controller, you could just look for that. So you would have something like:


// assets/controllers/autocomplete-input-controller.js

import { Controller } from "@hotwired/stimulus"
import $ from 'jquery';

export default class extends Controller {
    static values = { url: String }

    connect() {
        const url = this.urlValue;

        $(this.element).find('input').autocomplete({hint: false}, [
            {
                source: function(query, cb) {
                    $.ajax({
                        url: url+'?query='+query
                    }).then(function(data) {
                        cb(data.users);
                    });
                },
                displayKey: 'email',
                debounce: 500
            }
        ])
    }
}

The only change was looking for the "input" element from within this.element.

I think there are still other moving pieces... but let me know if this helps :).

Cheers!

Reply

Hi Ryan!
Do you think that most of third-party controllers are low quality?

Reply

Hey Eugem!

Hmm. I think that's a case-by-case basis. The stimulus-use library (not exactly controllers) are super high quality. https://stimulus-components... also look high quality. The controller in this chapter is... works really well... but it is *not* the highest quality. It lacks tests mostly, which I would expect any high quality library to have.

So, it's like PHP libraries or bundles - it varies widely. There are definitely both types out there.

Cheers!

Reply

Hello,
I am trying to use stimulus clipboard. It works pretty good but when I try to import it "lazy" using the symfony bridge it fails with an error coming from a file called blessing.js in stimulus core - so I am missing a blessing :(

I mean..

import Clipboard from 'stimulus-clipboard'```

This works

import { Clipboard } from '@symfony/stimulus-bridge/lazy-controller-loader?lazy=true&export=Clipboard!stimulus-clipboard'`

This doesn't work

import Clipboard from '@symfony/stimulus-bridge/lazy-controller-loader?export=Clipboard!stimulus-clipboard'```

This does work (without the curlies and without being lazy...)


I hope there is some error on my side ;)
Reply

Hey elkuku

That's odd, it makes me wonder if there's some bug on Symfony UX or Stimulus itself. Did you register the Clipboard module after importing it?
app.register('clipboard', Clipboard);

Reply
vLoght Avatar

Does autocomplete lib support `debouce` options like in original `search-preview` ?

Reply

Hey vLoght!

That's an excellent question that I completely forgot to address :). Yes, it does debouncing by default - 300ms - but it's not configurable (this library works super well, but it's dead-simple).

Cheers!

Reply

It can help for for people living in the past ⌛️

yarn upgrade @symfony/stimulus-bridge@2.1.0 --dev

https://github.com/symfony/...

Reply

Hey palaciospoa

IIRC this course IS using stimulus-bridge version 2.1.0 so what exact issue will fix your recomendation?

Cheers!

Reply

Just to mention that people that have this course since it was launched, have to look into their package dependencies.

I have the trouble of not having the feature of laziness via query parameter. BTW it is awesome great work 👍

Reply

Great! Thanks for your tip! BTW it's always better to download new code from course page that will help to avoid such issues!

Cheers!

Reply
Wru M. Avatar

Hi! It's possible combine SmartWizard with Webpack? I tried adding the following command "require("smartwizard"); and initialize $('#smartwizard').smartWizard();", but on website in console I have the following error:

Uncaught TypeError: jquery__WEBPACK_IMPORTED_MODULE_2___default(...)(...).SmartWizard is not a function

What I'm doing wrong?

Reply

Hey Gareth,

I personally didn't use SmartWizard jQuery plugin, but did you try to add ".autoProvidejQuery()" to your Webpack Encore config? See this page for more details:
https://symfonycasts.com/sc...

Please, don't forget to restart your webpack encore after this change! :) Does it help you?

Cheers!

Reply
Wru M. Avatar
Wru M. Avatar Wru M. | Victor | posted 2 years ago | edited

Thanks for help Victor, but effect it's that same. Console return error like above and before restarted webpack encore. Even I tried this code added to webpack, but still nothing.


    .autoProvideVariables({
        $: 'jquery',
        jQuery: 'jquery',
        'window.jQuery': 'jquery',
    })

Datatables works in a similar way and there is no problem with reading jQuery, but I had to add code to webpack.config.js


config.module.rules.unshift({
  parser: {
    amd: false,
  },
});

Could you add a smartWizard to your project and try to run it? If you have no problems, there may be something wrong with my webpack settings, or some additional library is missing.

Reply

Hey Wru M.!

I don't know that library, but here's my guess (this is from looking at their source code too). It seems like the usage is supposed to be:


import $ from 'jquery';

/* it looks like, when you import smartwizard, it returns a function */
import smartWizardFactory from 'smartwizard';

/* you can then call this function and pass it jQuery */
/* the result is that, I believe, it will modify the $ variable so that */
/* it has the smartWizard() method on it (e.g. $('#foo').smartWizard()); ) */

smartWizardFactory($);

This is a bit of a guess... but if I'm wrong you could do some debugging. This is the part of the code that should be executing when you import the "smartwizard" module - https://github.com/techlab/jquery-smartwizard/blob/21681e8aea67376d2d3743b4a27b7ecdc49babaf/dist/js/jquery.smartWizard.js#L26-L42 - that file would live at node_modules/smartwizard/dist/js/jquery.smartWizard.js in your project, and you could be some console.log() there to learn more.

Good luck!

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