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!
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.
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.
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.
Hey @dave!
In general, for a search bar, I would not use Symfony's Form component. It's just too simple of a situation (only one field and no validation errors), so creating a normal HTML input field and handling it manually in a controller will give you a lot of flexibility.
So, to answer your question, of course! The *how* depends entirely on how you're building your frontend. If you were using something like React to build the results in your table, then each time you type, you'd send an AJAX request, that AJAX request would return the current results based on that search in JSON, and in React, you would re-render using those results. There is also the DataTable JavaScript library, which would work in a very similar way: https://datatables.net/.
In all cases, in Symfony, you have an endpoint that is able to read the current search value and return the results based on that value. How you render that on the front-end depends on the JavaScript library.
Cheers!
I have a similar question, in this example we override the configureDefaults to add a custom data- class with the url we want to submit the Ajax call to.
In the example of a global search bar (accessible from every page), like you said Ryan, it is a simple html form element, Symfony Forms is not used.
How would you pass the autocomplete url when Forms is not used?
I currently have this snippet inside my base.html.twig:
<script>
let $searchbar_suggestion_url = "{{ path('searchbar_suggestions')|escape('js') }}";
</script>
It feels a bit "unclean", because of its location (base.html.twig).
Hey Mike P.
Well, you could just use Symfony forms for that, or add a data-url HTML attribute to your form and then do the same thing Ryan did.
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
}
}
is there any way how to update the results in the table while typing in the search bar with ajax and symfony