If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeFrom 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.
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 |
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.
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' | |
] | |
]); | |
} | |
} |
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.
Hey Bippo,
You can use Twig inheritance, in a base template you add a CSS and Script block with all of your dependencies, and then, any template that requires them can just inherit from that template. Or, you can create a twig file including the deps, and add it to your template by using the include
function
Cheers!
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!
Hey Cyril S.!
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/screencast/symfony-forms/autocomplete-ajax#adding-a-data-autocomplete-url-attribute
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/screencast/symfony-forms/autocomplete-ajax#codeblock-b3f8faa454
D) A data-display-key
, which will replace the hardcoded displayKey: 'email'
here: https://symfonycasts.com/screencast/symfony-forms/autocomplete-ajax#codeblock-b3f8faa454
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!
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.
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...
Hey Cyril S.!
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/screencast/javascript/delegate-selectors-ftw - 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!
Aahh! Set in webpack<br />import 'autocomplete.js/dist/autocomplete.jquery.min'<br />// import 'autocomplete.js' $(...).autocomplete is not a function<br />
I did not immediately understand the reason. But my code does not want to work. Night, 2:38 =(
Maybe it helps someone in future:
in app.jsimport 'autocomplete.js/dist/autocomplete.jquery';
not import 'autocomplete.js';
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!
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
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!
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..
Hey jacobbullock95!
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!
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
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?
I found my mistake. Finally, it is just a misspelling: I wrote
{% block javascript %}
instead of
{% block javascript %}
Hey Stephansav,
Ah, tricky misprint, difficult to notice! Glad you nailed it! And thank you for your feedback that you got it working.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.1
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.6
"symfony/console": "^4.0", // v4.1.6
"symfony/flex": "^1.0", // v1.17.6
"symfony/form": "^4.0", // v4.1.6
"symfony/framework-bundle": "^4.0", // v4.1.6
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.6
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.6
"symfony/validator": "^4.0", // v4.1.6
"symfony/web-server-bundle": "^4.0", // v4.1.6
"symfony/yaml": "^4.0", // v4.1.6
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
"symfony/dotenv": "^4.0", // v4.1.6
"symfony/maker-bundle": "^1.0", // v1.8.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.6
}
}
isnt there a nicer way to inject css and script into the view? e.g. instead of MANUALLY injecting, isnt there a way to put the dependencies somewhere else? because if you have 15 dependencies in a view, this can become ugly quickly. In laravel there is the @once notation that can be used potentially. As well components in general