lifo/typeahead-bundle
Symfony bundle that integrates Twitter Typeahead.js into forms and templates, adding AJAX-powered autocomplete and suggestion fields with minimal configuration. Provides helpers for connecting typeahead inputs to controllers and data sources.
Install and register the bundle (Symfony 2/3 style; for Symfony 4+ add it to config/bundles.php):
composer require lifo/typeahead-bundle
// app/AppKernel.php
$bundles = [
// ...
new Lifo\TypeaheadBundle\LifoTypeaheadBundle(),
];
There are two files to read before you write a line of code: Form/Type/TypeaheadType.php (the options and the defaults it injects) and Resources/views/Form/typeahead_widget.html.twig (the markup and the JS init). Everything the bundle does happens between those two files. The bundle is a thin wrapper — it does not ship a JS plugin, so your base layout still needs jQuery and a Bootstrap-3-compatible typeahead.js (bootstrap-3-typeahead is the common fork) loaded in that order.
First use case — an autocomplete text field backed by a JSON endpoint:
use Lifo\TypeaheadBundle\Form\Type\TypeaheadType;
$builder->add('city', TypeaheadType::class, [
'source' => '/api/cities?q=%QUERY', // %QUERY is replaced client-side
'minLength' => 2,
'items' => 8,
]);
The controller just returns a JSON array — strings, or {value, id} objects if you need to carry an identifier:
public function searchCities(Request $request)
{
$cities = $this->getDoctrine()->getRepository(City::class)
->search($request->query->get('q'), 20);
return $this->json(array_map(fn (City $c) => [
'id' => $c->getId(), 'value' => $c->getName(),
], $cities));
}
Label vs. value (the one pattern you will always need). Typeahead is a display widget, not a <select>. The input posts the label the user typed, not the id. The standard fix is a hidden field plus a typeahead:selected handler:
$builder
->add('city', TypeaheadType::class, ['source' => '/api/cities?q=%QUERY'])
->add('cityId', HiddenType::class, ['required' => false]);
$(document).on('typeahead:selected typeahead:autocompleted', 'input[data-provide="typeahead"]', function (e, item) {
$(this).closest('form').find('[name$="[cityId]"]').val(item.id);
});
On submit, resolve the id in a PRE_SUBMIT listener (or a DataTransformer) so the entity is what actually gets persisted — never trust the free-text label.
Form theme override instead of inline script. Don't hand-write <script> blocks in your templates. Override the typeahead_widget block in your form theme once, and pass plugin options through type options rather than hard-coding them per view. This is the cleanest extension point the bundle offers: it keeps all the JS configuration in PHP.
Collections / dynamically added rows. The bundle initialises on DOM ready, so rows added later by a collection prototype won't be wired up. Either re-run the init on collection:entry-added, or use the delegate-event style above ($(document).on(...)) which survives DOM insertion for the selection handler. Prefer delegating early — it's the difference between "works" and "mysteriously broken on the second row".
Validate what comes back. Add a NotBlank/custom constraint on cityId and re-fetch the entity server-side, then compare it against the submitted label. It is the only reliable guard against a user posting arbitrary text into a field the UI pretends is a picker.
Testing. The type does nothing but set view variables and attributes, so a TypeTestCase asserting the rendered attributes/options is sufficient; there's no need to mock HTTP.
%QUERY must survive URL encoding. If your endpoint is generated by the router, generate('api_cities', ['q' => '%QUERY']) will percent-encode the placeholder. Build the string manually or decode it once before passing it as the source option.[{value: ...}] vs. bare strings, or a Symfony profiler HTML wrapper on the response). Verify Content-Type: application/json.data-* attributes. The Twig theme emits the plugin config as data attributes; when a plugin option "doesn't work", confirm it actually reached the DOM before assuming a bundle bug. minLength, items, and displayKey are the usual suspects.AbstractType signatures. If you're on 4.4+/5.x, expect deprecation noise at minimum and possible hard breaks; pin it, wrap it, and keep a replacement plan. Its 21 stars and zero dependents mean you are the maintainer of last resort.items, but the endpoint should also LIMIT — otherwise a single keystroke pulls your whole table.How can I help you explore Laravel packages today?