Hooking up the AJAX Autocomplete
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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.
// ... 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.
// ... 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
.
// ... 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.
$(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.
// ... 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.
// ... 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.
// ... 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.
// ... 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')
.
// ... 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.
// ... 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()
.
// ... 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.
is there any way how to update the results in the table while typing in the search bar with ajax and symfony