Symfony UX Autocomplete: Live Search with Tom Select and Zero Custom JavaScript
AI generated
SF
{ }
Symfony · UX · Stimulus · Forms
Symfony UX Autocomplete
Live search with Tom Select, zero custom JavaScript

Symfony UX Autocomplete connects Doctrine entities, custom search logic and external APIs through a single form field type, delivering server side rendering, Stimulus wiring and Tom Select on the frontend without writing a single custom JavaScript module.

18 min read Symfony UX Autocomplete · Tom Select · Stimulus Symfony 7.x · PHP 8.4

1. Why Symfony UX Autocomplete exists

Symfony UX Autocomplete solves a problem that shows up in almost every larger form: a dropdown with thousands of entries no longer makes sense as a plain select field. Before Symfony UX, the usual fix was to wire up select2 or Choices.js by hand, write custom JavaScript initialization and implement the server communication with a hand rolled fetch call. Every project ended up with its own, slightly different solution and its own set of bugs.

Symfony UX Autocomplete wraps exactly this task into a reusable bundle. The form field type automatically generates the matching Stimulus attribute, the bundle ships a ready made controller that initializes Tom Select, and the server communication runs through a generated controller endpoint that wraps Doctrine repository queries. The result: live search inside forms without a single line of custom JavaScript, while still offering clearly defined PHP extension points for anything beyond the default case.

The difference from a plain JavaScript library lies in the integration depth. Symfony UX Autocomplete understands Doctrine entities, validation rules and CSRF protection out of the box, because it is part of the Symfony form system rather than an independent frontend widget bolted on the side.

2. Installation and basic configuration

Installing Symfony UX Autocomplete happens through Composer, and on AssetMapper projects the JavaScript dependencies are wired up automatically through the Symfony Asset Mapper. On Encore projects you additionally need to run yarn or npm to include Tom Select in the build. After installation the bundle automatically registers a Stimulus controller and exposes the form field type Symfony\UX\Autocomplete\Form\AutocompleteField along with the autocomplete option for EntityType.

One important point during basic configuration: Symfony UX Autocomplete automatically registers an internal controller under /_ux_entity_autocomplete/{alias} for every entity that uses autocomplete. This endpoint does not need to be added manually to routes.yaml, but the security layer still treats it like any other route. Anyone using autocomplete on sensitive entities therefore has to explicitly check that the generated endpoint is covered by firewalls and access control rules.


# Install Symfony UX Autocomplete
composer require symfony/ux-autocomplete

# AssetMapper projects: assets are wired automatically
bin/console importmap:require symfony/ux-autocomplete

# Encore projects only: build the JS integration
yarn add @symfony/ux-autocomplete --dev
yarn encore dev

3. Enabling autocomplete directly in EntityType

The fastest way into Symfony UX Autocomplete is the autocomplete option directly on EntityType. A plain dropdown with thousands of products or customers becomes a live search field without writing a dedicated class. Symfony generates the matching endpoint behind the scenes, searches through the entity's repository and delivers the results to Tom Select as JSON.

This simple form of Symfony UX Autocomplete fits the default case: searching across one or two fields, no complex sorting logic, no per row permission checks. Once the search logic gets more involved, for example combining several fields or checking permissions per row, you switch to the custom AutocompleteField class covered in section four.


// src/Form/OrderType.php
namespace App\Form;

use App\Entity\Customer;
use App\Entity\Order;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;

final class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('customer', EntityType::class, [
            'class' => Customer::class,
            'choice_label' => 'fullName',
            // Enable Symfony UX Autocomplete for this field
            'autocomplete' => true,
            'placeholder' => 'Search for a customer...',
        ]);
    }

    public function getBlockPrefix(): string
    {
        return '';
    }
}

4. A custom AutocompleteField class for complex searches

Once the search behind Symfony UX Autocomplete needs to do more than a plain LIKE query on one field, you create a custom class that extends AbstractType and implements the EntityAutocompleteFieldInterface. This class defines a createFilteredQueryBuilder method where arbitrarily complex Doctrine query builder logic can live, including joins across related entities, relevance based sorting, or filtering by the currently logged in user.

The decisive advantage of this variant of Symfony UX Autocomplete is that the search logic lives in exactly one place in the codebase and can be reused across multiple forms without duplicating options. The class can additionally override a getLabel method to assemble the displayed label per result individually, for example customer name plus customer number plus city on a single line.


// src/Form/CustomerAutocompleteField.php
namespace App\Form;

use App\Entity\Customer;
use App\Security\CurrentUserProvider;
use Doctrine\ORM\QueryBuilder;
use Symfony\UX\Autocomplete\Doctrine\EntityAutocompleteFieldInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;

final class CustomerAutocompleteField extends EntityType implements EntityAutocompleteFieldInterface
{
    public function __construct(
        private readonly CurrentUserProvider $currentUserProvider,
    ) {
    }

    public function getLabel(object $entity): string
    {
        /** @var Customer $entity */
        return sprintf('%s (%s, %s)', $entity->getFullName(), $entity->getCustomerNumber(), $entity->getCity());
    }

    public function createFilteredQueryBuilder(QueryBuilder $queryBuilder, string $query): void
    {
        $tenantId = $this->currentUserProvider->getCurrentTenantId();

        $queryBuilder
            ->andWhere('entity.tenant = :tenantId')
            ->setParameter('tenantId', $tenantId)
            ->andWhere('entity.fullName LIKE :query OR entity.customerNumber LIKE :query')
            ->setParameter('query', '%' . $query . '%')
            ->orderBy('entity.fullName', 'ASC')
            ->setMaxResults(20);
    }

    public function getOptionsForAutocomplete(): array
    {
        return [
            'class' => Customer::class,
            'choice_label' => 'fullName',
        ];
    }
}

5. Wiring in external APIs instead of Doctrine entities

Not every search behind Symfony UX Autocomplete has to run against your own database. For product data from an external PIM system, address suggestions from a geodata API, or item searches against Elasticsearch, the bundle lets you register a custom controller as the data source instead of relying on a Doctrine repository. The controller only needs to return the JSON shape that Tom Select expects for its result list.

This approach to Symfony UX Autocomplete fully decouples the frontend component from the data source. Instead of pointing at the internal entity endpoint, the Stimulus attribute points directly at a custom route that internally uses an HTTP client for the external API, normalizes the result and returns it as JSON. From the user's perspective the interaction looks identical to the entity based variant, while a completely different request runs under the hood.


// src/Controller/ProductSearchAutocompleteController.php
namespace App\Controller;

use App\Client\ExternalPimClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

final class ProductSearchAutocompleteController extends AbstractController
{
    public function __construct(
        private readonly ExternalPimClient $pimClient,
    ) {
    }

    #[Route('/autocomplete/products', name: 'app_autocomplete_products', methods: ['GET'])]
    public function __invoke(Request $request): JsonResponse
    {
        $query = (string) $request->query->get('query', '');

        if (mb_strlen($query) < 2) {
            return new JsonResponse(['results' => []]);
        }

        $products = $this->pimClient->search($query, limit: 15);

        // Tom Select expects a "results" array with id/text pairs
        $results = array_map(
            static fn (array $product): array => [
                'id' => $product['sku'],
                'text' => sprintf('%s (%s)', $product['name'], $product['sku']),
            ],
            $products,
        );

        return new JsonResponse(['results' => $results]);
    }
}

6. Configuring Tom Select: multiple, preload, rendering

Under the hood, Symfony UX Autocomplete uses Tom Select as its rendering library. Most Tom Select options can be controlled through data attributes on the generated element or through options on the form field type, without writing a custom Stimulus controller class. The multiple option enables tag based multi selection, and the tom_select_options option passes an associative array straight through to the Tom Select instance, for example for custom render callbacks.

For cases where a preselected value needs to be visible right on page load, Symfony UX Autocomplete supports preloading the current value by rendering its label on the server. This avoids the usual brief moment where only the ID is visible until JavaScript fetches the matching text. Anyone who needs custom render templates for the dropdown options, for example an avatar image with extra details, can supply render_option and render_item as JavaScript functions through the options.


{# templates/order/new.html.twig #}
{{ form_start(form) }}
  {{ form_row(form.customer, {
    attr: {
      'data-tom-select-multiple': false,
      'data-tom-select-preload': 'focus'
    }
  }) }}

  {{ form_row(form.products, {
    attr: { 'data-tom-select-multiple': true }
  }) }}
{{ form_end(form) }}

7. Styling with Tailwind, no extra CSS framework

Tom Select ships with its own default theme that visually clashes with a Tailwind driven, Hyvä style frontend. Instead of importing the full Tom Select CSS, Symfony UX Autocomplete projects deliberately override the generated Tom Select CSS classes with custom Tailwind utility classes. Tom Select produces predictable class names such as ts-control, ts-dropdown and ts-wrapper that can be adapted to your own design system through a thin CSS layer.

The important part of this approach to Symfony UX Autocomplete is to load only the minimal, unstyled base file from Tom Select rather than the full default stylesheet, and to layer your own Tailwind classes on top of it. This meaningfully shrinks the CSS bundle size and prevents conflicts between Tailwind's utility first approach and Tom Select's prebuilt classes.


/* assets/styles/autocomplete.css */
/* Minimal overrides on top of Tom Select's base (unstyled) CSS */

.ts-wrapper.single .ts-control {
  @apply rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm;
}

.ts-wrapper.focus .ts-control {
  @apply border-gray-700 ring-2 ring-gray-200;
}

.ts-dropdown {
  @apply mt-1 rounded-lg border border-gray-200 bg-white shadow-lg;
}

.ts-dropdown .option.active {
  @apply bg-gray-100 text-gray-900;
}

8. Performance: debounce, minimum length and caching

Without tuning, Symfony UX Autocomplete sends a request to the server on every single keystroke, which creates an unnecessary number of parallel requests when typing quickly. The bundle debounces input by default, and the wait time can be adjusted through the debounce option in milliseconds. On top of that, a configurable minimum search input length prevents thousands of database rows from being loaded after just one or two characters.

On the server side it is worth adding a short cache for frequently repeated search queries, for example through the Symfony cache layer with a TTL of a few seconds. This reduces database load for popular search terms without results feeling noticeably stale. Anyone running Symfony UX Autocomplete against an external API should use the cache to avoid exceeding that API's rate limits as well.

9. Symfony UX Autocomplete compared

Before committing to Symfony UX Autocomplete, it is worth looking at the alternatives commonly found in existing projects. Each approach has different consequences for maintenance effort, bundle size and integration depth with the Symfony form system.

Approach Integration Custom JS Code Bundle Size
select2 wired manually Loose, custom fetch needed A lot Medium
Choices.js manually Loose, custom fetch needed A lot Medium
Symfony UX Autocomplete Deep, form system native None Low
Custom Stimulus controller Medium, hand built A lot Variable
Native HTML datalist Very loose None None

The native datalist solution without any library at all seems tempting because it costs zero extra bytes of JavaScript, but in practice it fails on server side search, missing debouncing and inconsistent browser rendering. Symfony UX Autocomplete sits right between the two extremes: minimal custom code, yet full control over server logic and rendering exactly where it is needed.

Mironsoft

Symfony development with a modern UX frontend

Forms that feel like an SPA, without writing any custom JavaScript?

We integrate Symfony UX Autocomplete into existing forms, build custom AutocompleteField classes for complex searches, and connect external APIs cleanly to your Symfony project.

UX audit

Reviewing existing forms for autocomplete potential

Implementation

Custom AutocompleteField classes and external API wiring

Tailwind styling

Integrating Tom Select seamlessly into your design system

10. Summary

Symfony UX Autocomplete solves the classic problem of oversized dropdown lists by uniting Doctrine repositories, custom search logic and external APIs under one consistent Stimulus integration built on Tom Select. The simple case only needs the autocomplete option on EntityType, while more demanding requirements are handled by a custom AutocompleteField class with full query builder control over the search. External data sources can be wired in through a custom controller without touching the frontend integration at all.

The biggest advantage over manually wired JavaScript libraries lies in maintainability: Symfony UX Autocomplete stays part of the Symfony ecosystem, benefits from security updates through Composer, and needs no separate build process for its core functionality. Configuring debounce, minimum length and a server side cache on top gives you a live search that behaves just as reliably under load as it does under normal conditions.

Symfony UX Autocomplete — The essentials at a glance

Quick start

The autocomplete: true option on EntityType covers the default case, with zero custom JavaScript code.

Complex searches

A custom AutocompleteField class with createFilteredQueryBuilder for joins, permissions and custom labels.

External sources

A custom controller acting as data source for PIM systems, Elasticsearch or geodata APIs.

Performance

Debounce, minimum length and a server cache prevent unnecessary database or API load.

11. FAQ: Symfony UX Autocomplete

1What is Symfony UX Autocomplete?
A bundle that turns form fields into live search fields using Tom Select and Stimulus, with a ready made backend for Doctrine entities and custom extension points.
2Do I need custom JavaScript?
No, not in the default case. The bundled Stimulus controller initializes Tom Select automatically.
3Wiring in an external API instead of Doctrine?
Through a custom controller returning the JSON shape with a results list of id and text.
4Securing the endpoint properly?
The endpoint runs under /_ux_entity_autocomplete/{alias} and must be explicitly covered by access_control rules.
5Preventing too many requests while typing?
Through the debounce option in milliseconds and a minimum length for the search input.
6Is multi selection possible?
Yes, through the multiple option, which puts Tom Select into tag mode.
7Styling Tom Select with Tailwind?
Load only the unstyled base CSS file and override generated classes like ts-control with Tailwind utilities.
8AssetMapper or Encore?
Both are supported, on Encore the JS dependency additionally needs to be installed via yarn.
9Showing a preselected value without a reload?
The label of the current value is rendered on the server, no extra request needed.
10Is caching worth it?
Yes, especially with external APIs that have rate limits, a short server cache noticeably reduces load.