Autocomplete JavaScript

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

From a backend perspective, the custom field is done! When the user submits a string email address, the data transformer turns that into the proper User object, with built-in validation.

But from a frontend perspective, it could use some help. It would be way more awesome if this field had some cool JavaScript auto-completion magic where it suggested valid emails as I typed. So... let's do it!

Google for "Algolia autocomplete". There are a lot of autocomplete libraries, and this one is pretty nice. Click into their documentation and then to the GitHub page for autocomplete.js.

Many of you might know that Symfony comes with a great a JavaScript tool called Webpack Encore, which helps you create organized JavaScript and build it all into compiled files. We have not been using Encore in this tutorial yet. So I'm going to keep things simple and continue without it. Don't worry: the most important part of what we're about to do is the same no matter what: it's how you connect custom JavaScript to your form fields.

Adding the autocomplete.js JavaScript

Copy the script tag for jQuery, open templates/article_admin/edit.html.twig and override {% block javascripts %} and {% endblock %}. Call the {{ parent() }} function to keep rendering the parent JavaScript. Then paste in that new <script> tag.

... lines 1 - 2
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/autocomplete.js/0/autocomplete.jquery.min.js"></script>
... line 7
{% endblock %}
... lines 9 - 23

Yes, we are also going to need to do this in the new template. We'll take care of that in a little bit.

Now, if you scroll down a little on their docs... there it is! This page has some CSS that helps make all of this look good. Copy that, go to the public/css directory, and create a new file: algolia-autocomplete.css. Paste this there.

Include this file in our template as well: override {% block stylesheets %} and {% endblock %}. This time add a <link> tag that points to that file: algolia-autocomplete.css. Oh, and don't forget the parent() call - I'll add that in a second.

... lines 1 - 9
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/algolia-autocomplete.css') }}">
{% endblock %}
... lines 15 - 23

Finally, for the custom JavaScript logic, in the js/ directory, create a new file called algolia-autocomplete.js. Before I fill anything in here, include that in the template: a <script> tag pointing to js/algolia-autocomplete.js.

... lines 1 - 2
{% block javascripts %}
... lines 4 - 6
<script src="{{ asset('js/algolia-autocomplete.js') }}"></script>
{% endblock %}
... lines 9 - 23

Implementing autocomplete.js

Initial setup done! Head back to their documentation to find where it talks about how to use this with jQuery. It looks kinda simple: select an element, call .autcomplete() on it, then... pass a ton of options that tell it how to fetch and process the autocomplete data.

Cool! Let's do something similar! I'll start with the document.ready() block from jQuery just to make sure the DOM is fully loaded. Now: here is the key moment: how can we write JavaScript that can connect to our custom field? Should we select it by the id? Something else?

I like to select with a class. Find all elements with, how about, some .js-user-autocomplete class. Nothing has this class yet, but our field will soon. Call .autocomplete() on this, pass it that same hint: false and then an array. This looks a bit complex: add a JavaScript object with a source option set to a function() that receives a query argument and a callback cb argument.

Basically, as we're typing in the text field, the library will call this function and pass whatever we've entered into the text box so far as the query argument. Our job is to determine which results match this "query" text and pass those back by calling the cb function.

To start... let's hardcode something and see if it works! Call cb() and pass it an array where each entry is an object with a value key... because that's how the library wants the data to be structured by default.

$(document).ready(function() {
$('.js-user-autocomplete').autocomplete({hint: false}, [
{
source: function(query, cb) {
cb([
{value: 'foo'},
{value: 'bar'}
])
}
}
]);
});

Thanks to my imaginative code, no matter what we type, foo and bar should be suggested.

Adding the js- Class to the Field

And... we're almost... sorta done! In order for this to be applied to our field, all we need to do is add this class to the author field. No problem! Copy the class name and open UserSelectTextType. Here, we can set a default value for the attr option to an array with class set to js-user-autocomplete.

... lines 1 - 11
class UserSelectTextType extends AbstractType
{
... lines 14 - 33
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
... lines 37 - 40
'attr' => [
'class' => 'js-user-autocomplete'
]
]);
}
}

Field Options vs View Variables

Up until now, if we've wanted to add a class attribute, we've done it from inside of our Twig template. For example, open security/register.html.twig. For the form start tag, we're passing an attr variable with a class key. Or, for the fields, we're adding a placeholder attribute.

attr is one of a few things that can be passed either as a view variable or also as a field option. But, I want to be clear: options and variables are two different things. Go back and open the profiler. Click on, how about, the author field. We know that there is a set of options that we can pass to the field from inside the form class. And then, when you're rendering in your template, there is a different set of view variables. These are two different concepts. However, there is some overlap, like attr.

Behind the scenes, when you pass the attr option, that simply becomes the default value for the attr view variable. The attr option, just like the label and help options - exists just for the added convenience of being able to set these in your form class or in your template.

Anyways, thanks to the code in UserSelectTextType, our field should have this class. Let's try it! Close the profiler, refresh and... ah! I killed my page! The CSS is gone! I always do that! Go back to the template and add the missing parent() call: I don't want to completely replace the CSS from our layout.

Ok, try it again. Much better. And when we type into the field... yes! We get foo and bar no matter what we type. Awesome!

Next, hey: I like foo and bar as much as the next programmer. But we should probably make an AJAX call to fetch a true list of matching email addresses.

Leave a comment!

  • 2020-06-10 weaverryan

    Hey Digit Image!

    Ah, yes, the classic problem of attaching behavior to a DOM element *after* it's added to the page :). Basically, you will need to do this:

    Isolate all of the code inside the document(ready) into a function. Then, each time you add an element to the page, you call that function again, which will reinitialize the elements. You may also need to track which elements have *already* been initialized by setting some data key on the element (e.g. $(this).data('_autocomplete_initialized')) and then skipping elements with that data.

    There is another solution - called a "delegate selector" https://symfonycasts.com/sc... - that *almost* works... but it won't in this case. You really do need to call .autocomplete() manually each time a new element is added.

    If you have any questions about the proposed solution, let me know!

    Cheers!

  • 2020-06-10 Digit Image

    Finally, I decided to use the old fashion way: to call the init autocomplete method on each new created input. Maybe is there a better way with a sort of global listener but, at least, it works. Thanks for your help.

  • 2020-06-09 Digit Image

    Thanks for your answer. Your code is exactly what I already had and it works perfectly with all the inputs loaded with the page. But it fails with inputs added by javascript later, as the listener doesn't listen them :-(
    I wanted to make a global listener on document but I don't know which event to listen...

  • 2020-06-09 weaverryan

    Hey Digit Image!

    I think we could make that happen :). In fact, our setup already *almost* makes this possible.

    1) Give every input that you want 4 things:

    A) a generic class - like js-autocomplete.
    B) a data-autocomplete-url attribute, like we do already - https://symfonycasts.com/sc...
    C) a data-key attribute, which is set to the "key" on the AJAX call that you want to use - it will the hardcoded .users code here https://symfonycasts.com/sc...
    D) A data-display-key, which will replace the hardcoded displayKey: 'email' here: https://symfonycasts.com/sc...

    2) Then the JavaScript will just need some minor updates:


    $(document).ready(function() {
    $('.js-user-autocomplete').each(function() {
    var autocompleteUrl = $(this).data('autocomplete-url');
    $(this).autocomplete({hint: false}, [
    {
    source: function(query, cb) {
    $.ajax({
    url: autocompleteUrl+'?query='+query
    }).then(function(data) {
    cb(data[$(this).data('key')]);
    });
    },
    displayKey: $(this).data('display-key'),
    debounce: 500 // only request every 1/2 second
    }
    ])
    });
    });

    I may have missed something, but I think that should do it! Let me know!

    Cheers!

  • 2020-06-09 Digit Image

    Hi !
    Is there a way to attach autocomplete.js globally for all the text inputs of a page (with a class for example) even when they are created by JS after the page is loaded? If possible, I don't want to call an init fonction each time an input is created but make a sort of global listener. Help would be appreciated!

  • 2020-04-01 weaverryan

    Hey Jacob Bullock!

    Thanks for posting! It's always nice to see other people's solutions to problems in case it helps others :).

    > Perhaps something has changed in the way Forms interact with this js plugin in symfony 5?

    It is possible, but I'm not aware of anything. The important piece in this video is that we're creating a custom field ( UserSelectTextType) and adding the attr option to *its* configureOptions() method. We're *not* adding it in the main form class anywhere - e.g. ArticleFormType. This allows us (in ArticleFormType) to add the UserSelectTextType and *not* have to *also* pass the "attr" option - it's already the "default" value thanks to the attr option we've added to UserSelectTextType. If you *did* add the attr option to the configureOptions() method of ArticleFormType, it *would* definitely add it to the form tag. So, just put it on UserSelectTextType and nowhere else. Or, it's just as valid to NOT put it on that class, and *only* put it in the builder->add() section like you did.

    I'm not sure if that will clear anything up for you, but hopefully it will explain what might be going on :).

    Cheers!

  • 2020-03-27 Jacob Bullock

    jumping in here just in case this helps anyone else, i got a console.log return value of 2 (would be more if my form had more than 1 field) turns out was applying the class to both the entire form and the field inside it. Fixed my issue by moving this part of code


    'attr' => [
    'class' => 'js-user-autocomplete'
    ]

    up to the field in builder->add (DON'T SET IN RESOLVER OPTIONS)
    Console.log now returns 1 and it is only applied to the field needed and the autocomplete works like a charm!

    Perhaps something has changed in the way Forms interact with this js plugin in symfony 5?
    Anyway, investigate it is not a CSS class issue like mine was.. spent too long investigating javascript when it was all working well..

  • 2020-02-20 Victor Bocharsky

    Hey Irina,

    Thank you for this tip!

    Cheers!

  • 2020-02-20 Irina Serdiuk

    Maybe it helps someone in future:
    in app.js
    import 'autocomplete.js/dist/autocomplete.jquery';
    not
    import 'autocomplete.js';

  • 2019-11-18 Victor Bocharsky

    Hey Stephansav,

    Ah, tricky misprint, difficult to notice! Glad you nailed it! And thank you for your feedback that you got it working.

    Cheers!

  • 2019-11-17 stephansav

    Sorry, instead of {% block javascripts %}
    with a "s".

  • 2019-11-17 stephansav

    I found my mistake. Finally, it is just a misspelling: I wrote

    {% block javascript %} instead of

    {% block javascript %}

  • 2019-11-17 stephansav

    In my case, I have the same problem. But, I have neither 0 neither 1 when refreshing the page for:
    console.log($('.js-user-autocomplete').length);
    How can I solve this please?

  • 2019-10-30 Diego Aguiar

    Hey Ozornick

    I believe you are hitting a known issue related to old jQuery plugins and Encore. In this chapter Ryan explains what's the problem and how to fix it https://symfonycasts.com/sc...

    Cheers!

  • 2019-10-29 Ozornick

    Aahh! Set in webpack

    import 'autocomplete.js/dist/autocomplete.jquery.min'
    // import 'autocomplete.js' $(...).autocomplete is not a function

    I did not immediately understand the reason. But my code does not want to work. Night, 2:38 =(

  • 2019-02-15 weaverryan

    Sweet! Nice debugging! Now keep going! ;)

  • 2019-02-14 Peter

    Hi, Ryan,

    Thanks for the quick answer.

    I found the "error" and it was really a "general error".
    NoScript was active and blocked cdn.jsdelivr.net.
    A stupid mistake on my part.
    I'll add a script blocker check.

    But on this occasion: the courses are great!

    Best regards

    Peter

  • 2019-02-14 weaverryan

    Hey @Peter!

    Hmm. The fact that the previous user-entries appeared definitely makes me think that the JavaScript simply isn't taking effect for some reason. Do you have any JavaScript errors? The other thing I like to check when JavaScript silently doesn't work, is to make sure I'm selecting the element correctly. In this case, in `algolia-autocomplete.js`, between the first line (document ready) and the 2nd line (the .autocomplete() line), add this:


    console.log($('.js-user-autocomplete').length);

    If this says 0... then for some reason, your element is not being found on the page. If it is 1, then... well... the problem is somewhere else ;).

    Let me know!

    Cheers!

  • 2019-02-13 Peter

    Hello,

    i tried the sample, to show "foo" and "bar" but the javascript doesn't work and the previous user-entries appeared.
    Script-tags seems to be okay. The assignment of the class also is correct.

    Also checked for typos.
    Is there possibly a general error I could have?
    thanks in advance
    peter