ivaynberg/select2
Select2 is a jQuery-based replacement for native select boxes, adding searchable dropdowns, remote data loading, infinite scrolling, tagging, and custom templating. It works with any PHP or Laravel app and drops easily into forms to create fast, accessible, user-friendly select inputs.
This is the Composer distribution of the Select2 jQuery plugin (3.x-era), not a PHP package. There is no service provider, no facade, and no config file — it just drops select2.js / select2.css into vendor/ivaynberg/select2. Treat it as static front-end assets you happen to install with composer require.
Minimal setup:
composer require ivaynberg/select2public/ (there is no vendor:publish tag by default — either reference vendor/ivaynberg/select2/select2.js directly, or copy select2.js and select2.css into public/vendor/select2/):
cp -r vendor/ivaynberg/select2 public/vendor/select2
<link rel="stylesheet" href="{{ asset('vendor/select2/select2.css') }}">
<script src="{{ asset('vendor/jquery/jquery.min.js') }}"></script>
<script src="{{ asset('vendor/select2/select2.js') }}"></script>
<select>:
{{ Form::select('user_id', $users, null, ['class' => 'select2']) }}
@push('scripts')
<script>
$('.select2').select2({ width: '100%', placeholder: 'Search…', allowClear: true });
</script>
@endpush
Look here first: the plugin's ajax option and the expected JSON shape ({ results: [{ id, text }], more: true }). That contract between a Laravel route and Select2 is 90% of the day-to-day work.
First real use case: replace a <select> holding thousands of rows with an AJAX-backed searchable select. Don't render 10k <option>s — point Select2 at a route.
AJAX search backed by a controller.
// routes/web.php
Route::get('/api/users', [UserSearchController::class, 'index'])->name('users.search');
// UserSearchController
public function index(Request $request)
{
$term = $request->input('q');
$page = (int) $request->input('page', 1);
$users = User::query()
->when($term, fn ($q) => $q->where('name', 'like', "%{$term}%"))
->orderBy('name')
->paginate(20, ['id', 'name'], 'page', $page);
return response()->json([
'results' => $users->getCollection()->map(fn ($u) => [
'id' => $u->id,
'text' => $u->name,
]),
'more' => $users->hasMorePages(),
]);
}
$('#user_id').select2({
width: '100%',
minimumInputLength: 2,
ajax: {
url: '{{ route('users.search') }}',
dataType: 'json',
delay: 250,
data: params => ({ q: params.term, page: params.page || 1 }),
processResults: (data, params) => ({
results: data.results,
pagination: { more: data.more }
}),
cache: true
}
});
Dependent / cascading selects. Clear and re-init the child on parent change; don't try to mutate options by hand.
$('#country').on('change', function () {
$('#city').val(null).trigger('change');
$('#city').select2({ ajax: { url: `/api/cities?country=${this.value}`, ... } });
});
Tagging / multi-value input. tags: true with a plain <select multiple> is the simplest path; validate the array server-side ('tags.*' => 'string|max:40') since Select2 will happily create free-text values.
Pre-selection on edit forms. Select2 can't infer a label from an ID. Either render the single matching <option selected> server-side, or seed it explicitly:
$('#user_id').append(new Option('{{ $user->name }}', '{{ $user->id }}', true, true)).trigger('change');
Blade integration. Keep init code out of partials. Use @push('scripts') + @stack('scripts') in the layout, and guard against double-init with .hasClass('select2-hidden-accessible') when the same view re-renders (e.g. after validation errors).
Asset pipeline. If you're on Vite, don't require this Composer package — install the npm select2 package instead and import it in resources/js/app.js. Use the Composer package only on legacy/asset-pipeline setups where a plain <script> tag is fine.
initSelection (3.x) vs ajax.processResults handling of id/text (4.x); formatResult/formatSelection (3.x) vs templateResult/templateSelection (4.x). Most tutorials you'll find online are for 4.x and will silently do nothing here. Check $.fn.select2.defaults or the version string before copying snippets.$ at parse time. If your bundle loads jQuery after, you get select2 is not a function.X-CSRF-TOKEN via $.ajaxSetup or the ajax.headers option, otherwise Laravel returns 419 and Select2 just shows "The results could not be loaded."paginate() returns {data: [...], total: ..., current_page: ...} — Select2 wants results + more, and each item needs literal id and text keys. Map them explicitly.old('user_id') is a scalar/array, not a rendered option, so the label disappears. Re-seed the option server-side (see above) or the select renders blank while still holding the value.width: '100%' or nothing lines up. Select2 defaults to resolve, which breaks in flex/grid layouts. Also set dropdownParent: $('#modal') for selects inside Bootstrap modals, or the dropdown renders behind the modal.allowClear requires a placeholder — a placeholder attribute and allowClear: true for single selects; otherwise the clear "×" never appears.templateResult/formatResult with escapeMarkup: m => m and interpolate user-controlled text, you've reintroduced XSS. Keep the default escaping or use $('<div>').text(...).<select>; if you're swapping form sections with Livewire/Alpine/partial reloads, re-init and re-append options after every morph, or the widget is empty.resources/js/select2-init.js that reads data-* attributes off the select (data-url, data-placeholder, data-min-length) and initializes everything in one place — that's the closest thing to a "config" this package offers.select2@4 package (or a modern alternative) and treat the Composer install as a stopgap while you keep legacy forms working.How can I help you explore Laravel packages today?