Black Friday 2022 Offers 30% discount in SymfonyInsight yearly business plan (offer ends in 2 days)

Autocomplete <select>

Edit this page

Autocomplete <select>

Transform your EntityType, ChoiceType or any <select> element into an Ajax-powered autocomplete smart UI control (leveraging Tom Select):

Demo of an autocomplete-enabled select element

Installation

Before you start, make sure you have Symfony UX configured in your app.

Then install the bundle using Composer and Symfony Flex:

1
2
3
4
5
6
7
8
9
$ composer require symfony/ux-autocomplete

# Don't forget to install the JavaScript dependencies as well and compile
$ npm install --force
$ npm run watch

# or use yarn
$ yarn install --force
$ yarn watch

Usage in a Form (without Ajax)

Any ChoiceType or EntityType can be transformed into a Tom Select-powered UI control by adding the autocomplete option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Form/AnyForm.php
// ...

class AnyForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('food', EntityType::class, [
                'class' => Food::class,
                'placeholder' => 'What should we eat?',
+                'autocomplete' => true,
            ])

            ->add('portionSize', ChoiceType::class, [
                'choices' => [
                    'Choose a portion size' => '',,
                    'small' => 's',
                    'medium' => 'm',
                    'large' => 'l',
                    'extra large' => 'xl',
                    'all you can eat' => '∞',
                ],
+                'autocomplete' => true,
            ])
        ;
    }
}

That's all you need! When you refresh, the Autocomplete Stimulus controller will transform your select element into a smart UI control:

Screenshot of a Food select with Tom Select

Usage in a Form (with Ajax)

In the previous example, the autocomplete happens "locally": all of the options are loaded onto the page and used for the search.

If you're using an EntityType with many possible options, a better option is to load the choices via AJAX. This also allows you to search on more fields than just the "displayed" text.

To transform your field into an Ajax-powered autocomplete, you need to create a new "form type" class to represent your field. If you have MakerBundle installed, you can run:

1
$ php bin/console make:autocomplete-field

Or, create the field by hand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Form/FoodAutocompleteField.php
// ...

use Symfony\Component\Security\Core\Security;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;

#[AsEntityAutocompleteField]
class FoodAutocompleteField extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'class' => Food::class,
            'placeholder' => 'What should we eat?',

            // choose which fields to use in the search
            // if not passed, *all* fields are used
            //'searchable_fields' => ['name'],

            // if the autocomplete endpoint needs to be secured
            //'security' => 'ROLE_FOOD_ADMIN',

            // ... any other normal EntityType options
            // e.g. query_builder, choice_label
        ]);
    }

    public function getParent(): string
    {
        return ParentEntityAutocompleteType::class;
    }
}

There are 3 important things:

  1. The class needs the #[AsEntityAutocompleteField] attribute so that it's noticed by the autocomplete system.
  2. The getParent() method must return ParentEntityAutocompleteType.
  3. Inside configureOptions(), you can configure your field using whatever normal EntityType options you need plus a few extra options (see Form Options Reference).

After creating this class, use it in your form:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Form/AnyForm.php
// ...

class AnyForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
+            ->add('food', FoodAutocompleteField::class)
        ;
    }
}

Caution

Avoid passing any options to the 3rd argument of the ->add() method as these won't be used during the Ajax call to fetch results. Instead, include all options inside the custom class (FoodAutocompleteField).

Congratulations! Your EntityType is now Ajax-powered!

Styling Tom Select

In your assets/controllers.json file, you should see a line that automatically includes a CSS file for Tom Select which will give you basic styles.

If you're using Bootstrap, set tom-select.default.css to false and tom-select.bootstrap5.css to true:

1
2
3
4
"autoimport": {
    "tom-select/dist/css/tom-select.default.css": false,
    "tom-select/dist/css/tom-select.bootstrap5.css": true
}

To further customize things, you can override the classes with your own custom CSS and even control how individual parts of Tom Select render. See Tom Select Render Templates.

Form Options Reference

All ChoiceType, EntityType and TextType fields have the following new options (these can also be used inside your custom Ajax autocomplete classes, e.g. FoodAutocompleteField from above):

autocomplete (default: false)
Set to true to activate the Stimulus plugin on your select element.
tom_select_options (default: [])
Use this to set custom Tom Select Options. If you need to set an option using JavaScript, see Extending Tom Select.
options_as_html (default: false)
Set to true if your options (e.g. choice_label) contain HTML. Not needed if your autocomplete is AJAX-powered.
autocomplete_url (default: null)
Usually you don't need to set this manually. But, you could manually create an autocomplete-Ajax endpoint (e.g. for a custom ChoiceType), then set this to change the field into an AJAX-powered select.
no_results_found_text (default: 'No results found')
Rendered when no matching results are found. This message is automatically translated using the AutocompleteBundle domain.
no_more_results_text (default: 'No more results')
Rendered at the bottom of the list after showing matching results. This message is automatically translated using the AutocompleteBundle domain.

For the Ajax-powered autocomplete field classes (i.e. those whose getParent() returns ParentEntityAutocompleteType), in addition to the options above, you can also pass:

searchable_fields (default: null)
Set this to an array of the fields on your entity that should be used when searching for matching options. By default (i.e. null), all fields on your entity will be searched. Relationship fields can also be used - e.g. category.name if your entity has a category relation property.
security (default: false)

Secures the Ajax endpoint. By default, the endpoint can be accessed by any user. To secure it, pass security to a string role (e.g. ROLE_FOOD_ADMIN) that should be required to access the endpoint. Or, pass a callback and return true to grant access or false to deny access:

1
2
3
4
5
use Symfony\Component\Security\Core\Security;

'security' => function(Security $security): bool {
    return $security->isGranted('ROLE_FOO');
}
filter_query (default: null)

If you want to completely control the query made for the "search results", use this option. This is incompatible with searchable_fields:

1
2
3
4
5
6
7
8
'filter_query' => function(QueryBuilder $qb, string $query, EntityRepository $repository) {
    if (!$query) {
        return;
    }

    $qb->andWhere('entity.name LIKE :filter OR entity.description LIKE :filter')
        ->setParameter('filter', '%'.$query.'%');
}
max_results (default: 10)
Allow you to control the max number of results returned by the automatic autocomplete endpoint.
preload (default: focus)
Set to focus to call the load function when control receives focus. Set to true to call the load upon control initialization (with an empty search).

Using with a TextType Field

All of the above options can also be used with a TextType field:

1
2
3
4
5
6
7
8
9
10
11
12
$builder
    // ...
    ->add('tags', TextType::class, [
        'autocomplete' => true,
        'tom_select_options' => [
            'create' => true,
            'createOnBlur' => true,
            'delimiter' => ',',
        ],
        // 'autocomplete_url' => '... optional: custom endpoint, see below',
    ])
;

This <input> field won't have any autocomplete, but it will allow the user to enter new options and see them as nice "items" in the box. On submit, all of the options - separated by the delimiter - will be sent as a string.

You can add autocompletion to this via the autocomplete_url option - but you'll likely need to create your own custom autocomplete endpoint.

Extending Tom Select

The easiest way to customize Tom Select is via the tom_select_options option that you pass to your field. This works great for simple things like Tom Select's loadingClass option, which is set to a string. But other options, like onInitialize, must be set via JavaScript.

To do this, create a custom Stimulus controller and listen to one or both events that the core Stimulus controller dispatches:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// assets/controllers/custom-autocomplete_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    initialize() {
        this._onPreConnect = this._onPreConnect.bind(this);
        this._onConnect = this._onConnect.bind(this);
    }

    connect() {
        this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect);
        this.element.addEventListener('autocomplete:connect', this._onConnect);
    }

    disconnect() {
        // You should always remove listeners when the controller is disconnected to avoid side-effects
        this.element.removeEventListener('autocomplete:pre-connect', this._onConnect);
        this.element.removeEventListener('autocomplete:connect', this._onPreConnect);
    }

    _onPreConnect(event) {
        // TomSelect has not been initialized - options can be changed
        console.log(event.detail.options); // Options that will be used to initialize TomSelect
        event.detail.options.onChange = (value) => {
            // ...
        };
    }

    _onConnect(event) {
        // TomSelect has just been intialized and you can access details from the event
        console.log(event.detail.tomSelect); // TomSelect instance
        console.log(event.detail.options); // Options used to initialize TomSelect
    }
}

Note

The extending controller should be loaded eagerly (remove /* stimulusFetch: 'lazy' */), so it can listen to events dispatched by the original controller.

Then, update your field configuration to use your new controller (it will be used in addition to the core Autocomplete controller):

1
2
3
4
5
6
7
8
$builder
    ->add('food', EntityType::class, [
        'class' => Food::class,
        'autocomplete' => true,
+        'attr' => [
+            'data-controller' => 'custom-autocomplete',
+        ],
    ])

Or, if using a custom Ajax class, add the attr option to your configureOptions() method:

1
2
3
4
5
6
7
8
9
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'class' => Food::class,
+        'attr' => [
+            'data-controller' => 'custom-autocomplete',
+        ],
    ]);
}

Advanced: Creating an Autocompleter (with no Form)

If you're not using the form system, you can create an Ajax autocomplete endpoint and then initialize the Stimulus controller manually. This only works for Doctrine entities: see Manually using the Stimulus Controller if you're autocompleting something other than an entity.

To expose the endpoint, create a class that implements Symfony\UX\Autocomplete\EntityAutocompleterInterface and tag this service with ux.entity_autocompleter, including an alias option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace App\Autocompleter;

use App\Entity\Food;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\Security\Core\Security;
use Symfony\UX\Autocomplete\EntityAutocompleterInterface;

#[AutoconfigureTag('ux.entity_autocompleter', ['alias' => 'food'])]
class FoodAutocompleter implements EntityAutocompleterInterface
{
    public function getEntityClass(): string
    {
        return Food::class;
    }

    public function createFilteredQueryBuilder(EntityRepository $repository, string $query): QueryBuilder
    {
        return $repository
            // the alias "food" can be anything
            ->createQueryBuilder('food')
            ->andWhere('food.name LIKE :search OR food.description LIKE :search')
            ->setParameter('search', '%'.$query.'%')

            // maybe do some custom filtering in all cases
            //->andWhere('food.isHealthy = :isHealthy')
            //->setParameter('isHealthy', true)
        ;
    }

    public function getLabel(object $entity): string
    {
        return $entity->getName();
    }

    public function getValue(object $entity): string
    {
        return $entity->getId();
    }

    public function isGranted(Security $security): bool
    {
        // see the "security" option for details
        return true;
    }
}

Thanks to this, your can now autocomplete your Food entity via the ux_entity_autocomplete route and alias route wildcard:

1
{{ path('ux_entity_autocomplete', { alias: 'food' }) }}

Usually, you'll pass this URL to the Stimulus controller, which is discussed in the next section.

Manually using the Stimulus Controller

This library comes with a Stimulus controller that can activate Tom Select on any select or input element. This can be used outside of the Form component. For example:

1
2
3
4
<select
    name="food"
    {{ stimulus_controller('symfony/ux-autocomplete/autocomplete') }}
>

That's it! If you want the options to be autocompleted via Ajax, pass a url value, which works well if you create a custom autocompleter:

1
2
3
4
5
6
<select
    name="food"
    {{ stimulus_controller('symfony/ux-autocomplete/autocomplete', {
        url: path('ux_entity_autocomplete', { alias: 'food' })
    }) }}
>

Note

If you want to create an AJAX autocomplete endpoint that is not for an entity, you will need to create this manually. The only requirement is that the response returns JSON with this format:

1
2
3
4
5
6
{
    "results": [
        { "value": "1", "text": "Pizza" },
        { "value": "2", "text":"Banana"}
    ]
}

Once you have this, generate the URL to your controller and pass it to the url value of the stimulus_controller() Twig function, or to the autocomplete_url option of your form field. The search term entered by the user is passed as a query parameter called query.

Beyond url, the Stimulus controller has various other values, including tomSelectOptions. See the controller.ts file for the full list.

Unit testing

When writing unit tests for your form, using the TypeTestCase class, you consider registering the needed type extension AutocompleteChoiceTypeExtension like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// tests/Form/Type/TestedTypeTest.php
namespace App\Tests\Form\Type;

use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension;

class TestedTypeTest extends TypeTestCase
{
    protected function getTypeExtensions(): array
    {
        return [
            new AutocompleteChoiceTypeExtension(),
        ];
    }

    // ... your tests
}

Backward Compatibility promise

This bundle aims at following the same Backward Compatibility promise as the Symfony framework: https://symfony.com/doc/current/contributing/code/bc.html

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.