Buy Access to Course
29.

Hooking up the AJAX Autocomplete

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We now have an endpoint that returns all users as JSON. And we have some autocomplete JavaScript that... ya know... autocompletes entries for us. I have a crazy idea: let's combine these two so that our autocomplete uses that Ajax endpoint!

Adding a data-autocomplete-url Attribute

First: inside of the JavaScript, we need to know what the URL is to this endpoint. We could hardcode this - I wouldn't judge you for doing that - this is a no-judgment zone. But, there is a simple, clean solution.

In AdminUtilityController, let's give our new route a name: admin_utility_users. Now, idea time: when we render the field, what if we added a "data" attribute onto the input field that pointed to this URL? If we did that, it would be super easy to read that from JavaScript.

25 lines | src/Controller/AdminUtilityController.php
// ... lines 1 - 10
class AdminUtilityController extends AbstractController
{
/**
* @Route("/admin/utility/users", methods="GET", name="admin_utility_users")
// ... line 15
*/
public function getUsersApi(UserRepository $userRepository)
// ... lines 18 - 24
}

Let's do it! In UserSelectTextType, add another attribute: how about data-autocomplete-url set to... hmm. We need to generate the URL to our new route. How do we generate a URL from inside of a service? Answer: by using the router service. Add a second argument to the constructor: RouterInterface $router. I'll hit Alt+Enter to add that property and set it.

51 lines | src/Form/UserSelectTextType.php
// ... lines 1 - 12
class UserSelectTextType extends AbstractType
{
// ... line 15
private $router;
// ... line 17
public function __construct(UserRepository $userRepository, RouterInterface $router)
{
// ... line 20
$this->router = $router;
}
// ... lines 23 - 49
}

Oh, and if you can't remember the type-hint to use, at least make sure that you remember that you can run:

php bin/console debug:autowiring

to see a full list of type-hints. By the way, in Symfony 4.2, this output will look a little bit different, but contains the same info. If you search for the word "route" without the e... cool! We have a few different type-hints, but they all return the same service anyways.

Now that we've injected the router, down below, use $this->router->generate() and pass it the new route name: admin_utility_users.

51 lines | src/Form/UserSelectTextType.php
// ... lines 1 - 36
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ... lines 40 - 43
'attr' => [
// ... line 45
'data-autocomplete-url' => $this->router->generate('admin_utility_users')
]
]);
}
// ... lines 50 - 51

Let's check it out! Refresh, inspect that field and ... perfect! We have a shiny new data-autocomplete-url attribute.

Making the AJAX Call

Let's head to our JavaScript! I'm going to write this a little bit different - though it would work either way: let's find all of the elements... there will be just one in this case... and loop over them with .each(). Indent the inner code, then close the extra function.

20 lines | public/js/algolia-autocomplete.js
$(document).ready(function() {
$('.js-user-autocomplete').each(function() {
// ... lines 3 - 17
});
});

Now we can change the selector to this and... yea! We're basically doing the same thing as before. Inside the loop, fetch the URL with var autocompleteUrl = $(this).data() to read that new attribute.

20 lines | public/js/algolia-autocomplete.js
// ... line 1
$('.js-user-autocomplete').each(function() {
var autocompleteUrl = $(this).data('autocomplete-url');
// ... lines 4 - 17
});
// ... lines 19 - 20

Finally, clear out the source attribute. Since we're using jQuery already, let's use it to make the AJAX call: $.ajax() with a url option set to autocompleteUrl. That's it!

To handle the result, chain a .then() onto the Promise and pass a callback with a data argument. Let's see: our job is to execute the cb callback and pass it an array of the results.

Remember: in the controller, I'm returning all the user information on a users key. So, let's return data.users: that should return this entire array of data.

20 lines | public/js/algolia-autocomplete.js
// ... lines 1 - 4
$(this).autocomplete({hint: false}, [
{
source: function(query, cb) {
$.ajax({
url: autocompleteUrl
}).then(function(data) {
cb(data.users);
});
},
// ... lines 14 - 15
}
])
// ... lines 18 - 20

But also remember that, by default, the autocomplete library expects each result to have a value key that it uses. Obviously, our key is called email. To change that behavior, add displayKey: 'email'. I'll also add debounce: 500 - that will make sure that we don't make AJAX requests faster than once per half a second.

20 lines | public/js/algolia-autocomplete.js
// ... lines 1 - 4
$(this).autocomplete({hint: false}, [
{
// ... lines 7 - 13
displayKey: 'email',
debounce: 500 // only request every 1/2 second
}
])
// ... lines 18 - 20

Ok... I think we're ready! Let's try this! Move back to your browser, refresh the page and clear out the author field... "spac"... we got it! Though... it still returns all of the users - the geordi users should not match.

Filtering the Users

That's no surprise: our endpoint always returns every user. No worries - this is the easiest part! Go back to the JavaScript. The source function is passed a query argument: that's equal to whatever is typed into the input box at that moment. Let's use that! Add a '?query='+query to the URL.

20 lines | public/js/algolia-autocomplete.js
// ... lines 1 - 6
source: function(query, cb) {
$.ajax({
url: autocompleteUrl+'?query='+query
// ... lines 10 - 11
});
},
// ... lines 14 - 20

Back in AdminUtilityController, to read that, add a second argument, the Request object from HttpFoundation. Then, let's call a new method on UserRepository, how about findAllMatching(). Pass this the ?query= GET parameter by calling $request->query->get('query').

26 lines | src/Controller/AdminUtilityController.php
// ... lines 1 - 8
use Symfony\Component\HttpFoundation\Request;
// ... lines 10 - 11
class AdminUtilityController extends AbstractController
{
// ... lines 14 - 17
public function getUsersApi(UserRepository $userRepository, Request $request)
{
$users = $userRepository->findAllMatching($request->query->get('query'));
// ... lines 21 - 24
}
}

Nice! Copy the method name and then open src/Repository/UserRepository.php. Add the new public function findAllMatching() and give it a string $query argument. Let's also add an optional int $limit = 5 argument, because we probably shouldn't return 1000 users if 1000 users match the query. Advertise that this will return an array of User objects.

76 lines | src/Repository/UserRepository.php
// ... lines 1 - 14
class UserRepository extends ServiceEntityRepository
{
// ... lines 17 - 33
/**
* @return User[]
*/
public function findAllMatching(string $query, int $limit = 5)
{
// ... lines 39 - 44
}
// ... lines 46 - 74
}

Inside, it's pretty simple: return $this->createQueryBuilder('u'), ->andWhere('u.email LIKE :query') and bind that with ->setParameter('query') and, this is a little weird, '%'.$query.'%'.

Finish with ->setMaxResults($limit), ->getQuery() and ->getResult().

76 lines | src/Repository/UserRepository.php
// ... lines 1 - 36
public function findAllMatching(string $query, int $limit = 5)
{
return $this->createQueryBuilder('u')
->andWhere('u.email LIKE :query')
->setParameter('query', '%'.$query.'%')
->setMaxResults($limit)
->getQuery()
->getResult();
}
// ... lines 46 - 76

Done! Unless I've totally mucked things up, I think we should have a working autocomplete setup! Refresh to get the new JavaScript, type "spac" and... woohoo! Only 5 results! Let's get the web debug toolbar out of the way. I love it!

Next: there's one other important method you can override in your custom form field type class to control how it renders. We'll use it to absolutely make sure our autocomplete field has the HTML attributes it needs, even if we override the attr option when using the field.