danilovl/select-autocompleter-bundle
Symfony bundle that turns standard form fields into Select2 autocompleters, loading choices on demand via remote AJAX instead of rendering huge option lists. Ships with sensible defaults for properties, limits, widgets, and CDN assets.
Installation:
composer require danilovl/select-autocompleter-bundle
Register the bundle in config/bundles.php:
Danilovl\SelectAutocompleterBundle\SelectAutocompleterBundle::class => ['all' => true],
Basic Configuration:
Add to config/packages/danilovl_select_autocompleter.yaml:
danilovl_select_autocompleter:
select2:
theme: "bootstrap4" # or 'bootstrap5', 'classic', 'bootstrap4-dark'
width: "100%"
placeholder: "Search..."
First Use Case:
Create a form type extending AbstractAutocompleterType:
use Danilovl\SelectAutocompleterBundle\Form\Type\AbstractAutocompleterType;
class UserAutocompleteType extends AbstractAutocompleterType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => User::class,
'choice_label' => 'fullName',
'ajax_route' => 'user_autocomplete',
'min_input_length' => 2,
]);
}
}
Route for Autocomplete:
Define a route in config/routes.yaml:
user_autocomplete:
path: /api/users/autocomplete
controller: App\Controller\UserAutocompleteController::class
methods: GET
Controller:
use Danilovl\SelectAutocompleterBundle\Controller\AutocompleteControllerTrait;
class UserAutocompleteController extends AbstractController
{
use AutocompleteControllerTrait;
public function __invoke(Request $request, UserRepository $userRepository)
{
return $this->handleAutocomplete($request, $userRepository, 'fullName');
}
}
Use in Form:
$builder->add('user', UserAutocompleteType::class);
Replace standard EntityType with AbstractAutocompleterType:
// Before
$builder->add('user', EntityType::class, ['class' => User::class]);
// After
$builder->add('user', UserAutocompleteType::class);
Override default options in configureOptions:
$resolver->setDefaults([
'ajax_route' => 'custom_route',
'min_input_length' => 1,
'multiple' => true,
'allow_clear' => true,
'select2_options' => [
'ajax' => [
'dataType' => 'json',
'delay' => 250,
],
],
]);
Pass dynamic parameters via ajax_route_parameters:
$resolver->setDefaults([
'ajax_route' => 'department_autocomplete',
'ajax_route_parameters' => ['company_id' => $companyId],
]);
Customize the response format in the controller:
public function __invoke(Request $request, ProductRepository $productRepository)
{
$results = $productRepository->findByName($request->query->get('term'));
return $this->json(array_map(function ($product) {
return [
'id' => $product->getId(),
'text' => $product->getName(),
'price' => $product->getPrice(),
];
}, $results));
}
Update the form type to handle custom fields:
$resolver->setDefaults([
'template' => '@DanilovlSelectAutocompleter/autocompleter_widget.html.twig',
'select2_options' => [
'templateResult' => 'customTemplate',
'templateSelection' => 'customTemplate',
],
]);
Add custom Twig templates (e.g., templates/custom_autocomplete.html.twig):
{# templates/custom_autocomplete.html.twig #}
<div class="custom-autocomplete">
{{ form_widget(form) }}
<span class="price">{{ data.price }}</span>
</div>
Use the bundle with API Platform entities:
$resolver->setDefaults([
'class' => Product::class,
'ajax_route' => 'api_products',
'ajax_route_parameters' => [],
'choice_label' => 'name',
'api_platform' => true, // Enable API Platform integration
]);
Implement pagination in the autocomplete controller:
public function __invoke(Request $request, UserRepository $userRepository)
{
$page = $request->query->getInt('page', 1);
$limit = 10;
$users = $userRepository->findByNamePaginated(
$request->query->get('term'),
$page,
$limit
);
$data = array_map(function ($user) {
return ['id' => $user->getId(), 'text' => $user->getFullName()];
}, $users);
return $this->json([
'results' => $data,
'pagination' => [
'page' => $page,
'last_page' => ceil($users->getTotalItems() / $limit),
],
]);
}
Update the form type to handle pagination:
$resolver->setDefaults([
'select2_options' => [
'ajax' => [
'data' => function ($params) {
return [
'term' => $params['term'],
'page' => $params['page'] ?? 1,
];
},
'processResults' => function ($data) {
return [
'results' => $data['results'],
'pagination' => [
'more' => $data['pagination']['page'] < $data['pagination']['last_page'],
],
];
},
],
],
]);
Combine with Symfony UX for enhanced interactivity:
// assets/controllers/autocomplete_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.element.addEventListener('change', (event) => {
// Handle selection changes
});
}
}
Update Twig template:
{{ form_widget(form) }}
{{ stimulus_controller('autocomplete', {'value': form.vars.value|default('')}) }}
Listen to form events for dynamic updates:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if ($data) {
$form->add('custom_field', TextType::class, [
'data' => $data->getCustomField(),
]);
}
});
Customize error messages:
$resolver->setDefaults([
'error_bubbling' => true,
'constraints' => [
new NotBlank(),
new Callback([$this, 'validateUser']),
],
]);
public function validateUser($value, ExecutionContextInterface $context)
{
if ($value && !$value->isActive()) {
$context->buildViolation('User is inactive.')
->atPath('user')
->addViolation();
}
}
Offload heavy processing to the server:
$resolver->setDefaults([
'ajax_route' => 'user_autocomplete_processed',
'select2_options' => [
'ajax' => [
'beforeSend' => function ($jqXHR, settings) {
settings.data.push({ name: 'process', value: 'heavy' });
},
],
],
]);
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
allow_origin: ['*']
allow_methods: ['GET', 'POST']
allow_headers: ['Content-Type']
expose_headers: []
max_age: 3600
hosts: []
origin_regex: false
Install NelmioCorsHow can I help you explore Laravel packages today?