public-repo/select2
A Laravel package for integrating Select2 dropdowns into your forms, with helpers for AJAX-loaded options, model binding, and searchable selects. Quickly add rich, user-friendly select inputs without wiring up JavaScript yourself.
Read this first: public-repo/select2 is a Symfony bundle (a fork of select2entity-bundle), not a Laravel package. There is no service provider, no facade, no Blade directive, and composer require will not wire anything into a Laravel app. Every capability described in the README — Blade-friendly helpers, model binding, old-input handling — is describing the pattern of a Select2 integration, not code you can require and call. For a Laravel developer, the package is a reference implementation you reimplement natively in a few hours, not a dependency you install.
If you need to read the Symfony code to understand the pattern, look at src/Form/Type/Select2EntityType.php (configureOptions documents every option), src/Resources/views/ (the Twig form theme emitting data-* attributes), and src/Resources/public/ (bundled Select2 version). Treat these as spec, not vendor code.
The Laravel mirror — your actual getting-started path:
npm install select2 jquery
Include both in your layout (Vite/app.js is fine — see Gotchas re: global $).Route::get('/users/search', [UserSearchController::class, 'index'])
->middleware('auth')->name('users.search');
public function index(Request $request)
{
$term = $request->string('q')->toString();
return User::query()
->when($term, fn ($q) => $q->where('name', 'like', "{$term}%"))
->orderBy('name')->limit(20)
->get(['id', 'name'])
->map(fn ($u) => ['id' => $u->id, 'text' => $u->name]);
}
<select> bound to your form field and init Select2 with ajax: { url, data: params => ({ q: params.term }) }.First use case: a form field currently rendering 10,000+ <option> tags (user assignment, category picker, related-record lookup). You swap the eager-loaded <select> for a typeahead that queries the JSON endpoint. The submitted value (id or id[]) is unchanged, so controllers and validation need no edits.
Server endpoint contract. Every Select2 AJAX endpoint returns exactly { "results": [ { "id": ..., "text": ... } ], "pagination": { "more": true|false } }. If you skip pagination.more, infinite scroll silently fails. Guard term before it hits the query builder — like "%{$term}%" is an injection-free Eloquent bind but still a full-table scan.
Blade + old-input round-trip. This is the piece to get right. Select2 does not pre-populate <option>s from a POSTed array unless you render the selected options server-side. A robust pattern:
<select name="user_id" id="user-select" class="form-select">
@if (old('user_id') && $user = \App\Models\User::find(old('user_id')))
<option value="{{ $user->id }}" selected>{{ $user->name }}</option>
@endif
</select>
For multiple selects, loop over old('user_ids', []) and fetch each label. Cache these lookups per request.
Model binding & re-population. On edit forms, pre-render the currently selected record(s) as the only <option>(s). Do not render the full list — that defeats the purpose. Select2 replaces the rest on search.
Validation errors. Select2 hides the underlying <select>, so a Laravel validation error message must sit outside the widget (near the label), not attached to the hidden control. Use @error('user_id') is-invalid @enderror on a wrapper <div>.
Dependent selects (cascading). When one Select2 drives another (country → city), clear and re-init the child on change, sending the parent's value in ajax.data. Debounce by setting delay: 300.
Remote search vs. preloaded data. If your option set is under ~500 rows, skip AJAX entirely — render <option>s and init plain Select2 with tags: false. Only reach for the AJAX pattern above a threshold where page weight matters. This mirrors the package's own guidance.
Localization. Set language in the JS init and load the matching Select2 language file (or override the loadingMore, noResults, searching strings). The Symfony package exposes this as a config tree; in Laravel you pass it directly to .select2({ language: 'fr' }).
Framework mismatch is the headline gotcha. Do not composer require public-repo/select2 in a Laravel project. It will install (Composer doesn't care about the framework) but it will pull in Symfony Form/DI contracts and register nothing. If a teammate points at the GitHub description and asks to use it, that conversation ends at "it's a Symfony bundle." This belongs in your dependency-risk register, not your composer.json.
jQuery global. Select2 v4 needs $ / jQuery on the window before it loads. With Vite this is a classic failure: you import select2 but window.$ is undefined at module-eval time, and you get a silent "no widget" page with no console error if tree-shaking drops it. Fix:
import $ from 'jquery';
window.$ = window.jQuery = $;
import 'select2';
Load Select2 after assigning the globals.
AJAX endpoint authorization. The bundle (and the Select2 pattern generally) does not enforce authorization or result caps — that is your job. Every endpoint must:
->where('team_id', auth()->user()->team_id)).->limit(20)). Unbounded LIKE over a large table with a slow DB is a trivial DoS.throttle:60,1 on the route).The "typed but didn't select" failure. A user types a name, doesn't click the suggestion, submits. Select2 sends the last selected id, which may be empty or stale — the typed text is discarded silently. On nullable foreign keys this looks like "the field reset itself." Mitigations: minimumInputLength + allowClear, plus a server-side exists:users,id rule on the submitted value.
Model binding on validation redirect. After a failed FormRequest, Laravel redirects with old input. If you don't re-render the selected <option> in Blade (above), the select appears blank and users re-pick — then complain the value "didn't save." The value did save; the widget just couldn't display it. This is the single most common support ticket for Select2 integrations.
CSP / asset pipelines. Select2 injects inline styles and its own dropdown DOM. If you run a strict Content-Security-Policy with no style-src 'unsafe-inline', the dropdown renders unstyled or not at all. Test behind your real production CSP headers early. AssetMapper/importmap setups need explicit jquery pinning — Select2 assumes a global.
Debugging checklist: open DevTools → Network, trigger a search, and confirm the request fires and returns the {results:[…]} shape. If the request fires but the dropdown is empty, log the raw response — a Laravel resource returning {data: […]} instead of {results: […]} is the classic off-by-one-shape bug. If no request fires, your init selector doesn't match the element (common when Blade renders a different id than the JS expects — use one source of truth for the id).
Extension points in Laravel. Where the Symfony package gives you a FormType, options array, and Twig theme to extend, Laravel gives you: a Blade component (<x-select2>), a Rule for validating the id exists, and a JS module that reads data-* attributes off the rendered <select>. Those three replace the entire bundle surface. Build them once, reuse across every form — that is the "day-to-day leverage" you actually get here.
How can I help you explore Laravel packages today?