Livewire Packagev3.0.0MIT License

Livewire Select2

Livewire Select2 turns a <select> in a Livewire component into a Select2 widget that stays in sync with the component. Put data-pharaonic="select2" and wire:model on the select, load one Blade directive, and the widget follows the property in both directions: what the user picks reaches the component, and what the server sets shows in the widget. It is built on Livewire Assistant, which finds the elements, binds each one to the component that owns it, and cleans them up when Livewire removes them.

Two-way binding

User choices update the property, and values the server sets update the widget, without echo requests.

Native wire:model

Use wire:model with its usual modifiers, or data-property, on single and multiple selects and dot paths.

Dynamic and dependent selects

Selects that Livewire adds, removes, or re-renders with new options are mounted, cleaned up, or rebuilt for you.

Right component, always

Each select binds to the component around it, so nested components and many selects per page just work.

Images and templates

Show an image beside each option without JavaScript, or name a template function once and reuse it.

No Node build required

The script ships prebuilt in the Composer package, or as an ES module for bundlers such as Vite.

Livewire 3 line

This documentation covers the 3.x line, which supports Livewire 3. Each Livewire major gets its own line. Coming from 1.2? See Upgrading from 1.2.

#Installation

Install the package with Composer, keep jQuery and Select2 on the page, and load the scripts once.

#Requirements

  • PHP 8.1 to 8.5, as supported by your Laravel version
  • Laravel 10.15+, 11, 12, or 13
  • Livewire 3.8.3 or later (see Compatibility)
  • jQuery and Select2 4, loaded by your application
  • A modern browser (ES2019); Internet Explorer is not supported

#Composer Installation

Terminal
composer require pharaonic/livewire-select2

Composer also installs pharaonic/livewire-assistant. Laravel discovers both service providers automatically. There is nothing to publish and no configuration file.

Composer and unmaintained Laravel versions

Livewire 3.8.3 and later, Laravel 12, and Laravel 13 have no known advisories. Every Laravel 10 and 11 release has security advisories that no release in its major fixes, and recent Composer versions refuse to install or update them until you allow them. If you accept the risk, allow laravel/framework, the only package to allow on Laravel 10 or 11 with Livewire 3:

Terminal
composer config --json policy.advisories.ignore '{"laravel/framework": "No fixed release within this Laravel major."}'

Read Compatibility before accepting the risk.

#Load the Scripts

Load jQuery and Select2 the way you already do, then add @livewireSelect2Scripts to your layout. It can go before or after Livewire's scripts, whether Livewire injects them automatically or you place @livewireScripts yourself.

resources/views/layouts/app.blade.php
1<head>
2 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css">
3 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
4 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>
5 @livewireStyles
6</head>
7<body>
8 {{ $slot }}
9 
10 @livewireScripts
11 @livewireSelect2Scripts
12</body>

The directive renders two <script> tags: the Livewire Assistant runtime and this integration. Each is rendered once per page, even when layouts, partials, and @livewireAssistantScripts ask for it again.

1<script src="https://example.com/pharaonic/livewire-assistant/assistant.js?id=3f9a1c2b7d4e5f60" data-navigate-once="true"></script>
2<script src="https://example.com/pharaonic/livewire-select2/select2.js?id=0d8eee40cb71f6d7" data-navigate-once="true"></script>

jQuery and Select2 are read from window.jQuery when a select mounts, not when the script loads, so their order relative to the directive does not matter. If they are still missing when the page has finished loading, the select is reported as mount-failed with the select2-missing cause.

#Vite and Other Bundlers

When jQuery and Select2 come from npm, expose jQuery on window and attach Select2 to that same jQuery. The package never uses $, so you do not need a global $.

resources/js/app.js
1import jQuery from 'jquery'
2import select2 from 'select2'
3 
4window.jQuery = jQuery
5select2()

Keep @livewireSelect2Scripts in the layout, or import the ES modules instead (see below).

#Content Security Policy

Pass a nonce when your CSP uses one. Both tags get it, escaped.

resources/views/layouts/app.blade.php
1@livewireSelect2Scripts(['nonce' => $cspNonce])

nonce is the only option. An unknown option or a non-string nonce throws an InvalidArgumentException while the view renders. The scripts never use eval or inline code.

#How the Script Is Served

RouteName
GET /pharaonic/livewire-select2/select2.jspharaonic.livewire-select2.script

The route serves dist/livewire-select2.js from the installed package, without session or cookies. The id query value is a hash of the file: a matching id is cached for a year, anything else is served with no-cache. After composer update the hash changes, so browsers fetch the new script on the next page load. Nothing needs publishing, and the route works with php artisan route:cache.

#ES Module

If you bundle your JavaScript, import the ES modules from the Composer packages instead of using the directive. Import the Assistant too: the integration registers with it but never loads it.

resources/js/app.js
1import jQuery from 'jquery'
2import select2 from 'select2'
3import '../../vendor/pharaonic/livewire-assistant/dist/livewire-assistant.esm.js'
4import { define } from '../../vendor/pharaonic/livewire-select2/dist/livewire-select2.esm.js'
5 
6window.jQuery = jQuery
7select2()

The ES module and the browser build share one integration per page, so loading both is harmless.

Installation Complete

Mark a select with data-pharaonic="select2" and you are done. See Basic Usage.

#Basic Usage

A Select2 field is a normal <select> with data-pharaonic="select2" and wire:model, wrapped in wire:ignore.

#The Component

app/Livewire/Profile.php
1<?php
2 
3namespace App\Livewire;
4 
5use Livewire\Component;
6 
7class Profile extends Component
8{
9 public $country = '2';
10 
11 public $languages = ['ar'];
12 
13 public function render()
14 {
15 return view('livewire.profile');
16 }
17}

#The View

resources/views/livewire/profile.blade.php
1<div>
2 <div wire:ignore>
3 <select data-pharaonic="select2" wire:model.live="country" data-placeholder="Choose a country" data-clear>
4 <option value=""></option>
5 <option value="1">Egypt</option>
6 <option value="2">Jordan</option>
7 </select>
8 </div>
9 
10 <div wire:ignore>
11 <select multiple data-pharaonic="select2" wire:model="languages">
12 <option value="ar">Arabic</option>
13 <option value="en">English</option>
14 </select>
15 </div>
16</div>

That is all. The first select shows Jordan; picking Egypt sets $country to "1" right away; $this->country = '2' on the server selects Jordan again. The second select holds an array, sent with the component's next request, because plain wire:model is deferred on Livewire 3. See Binding.

#Why wire:ignore

Livewire updates a component's HTML after each request to match the server. Select2 hides the <select> and adds its own markup next to it, which that update would remove. wire:ignore tells Livewire to leave the block alone, so the widget survives every request. The package keeps the value in sync through the component, not through the HTML.

A select can also live outside wire:ignore. The package then rebuilds the widget after each Livewire update, which is useful when the server changes its options. See Dynamic Selects.

#The Attributes

AttributePurpose
data-pharaonic="select2"Marks the element; required
wire:model="property"The property to bind, with its modifiers
data-property="property"The property to bind, when you do not use wire:model
data-tags, data-clear, data-search-off, data-parentShorthands for common Select2 options (see Options)
data-placeholder and other Select2 data-* attributesRead by Select2 itself
data-options='{...}'A JSON object passed to Select2, for everything else
data-component-idOptional: bind to a specific component instead of the one around the select ($this->getId() in a Livewire 3 view)

Without wire:model or data-property, the select becomes a Select2 widget that is not bound to anything.

Placeholders need an empty option

Select2 shows the placeholder of a single select only when it has an empty <option value="">. Put one first.

#Labels and Validation Messages

Livewire never updates anything inside wire:ignore, including an @error message placed there. Keep labels and messages outside the ignored block, so they update with the component:

1<label for="country">Country</label>
2 
3<div wire:ignore>
4 <select id="country" data-pharaonic="select2" wire:model.live="country">...</select>
5</div>
6 
7@error('country') <span class="text-danger">{{ $message }}</span> @enderror

#Binding

The package binds a select to one component property, in both directions.

#wire:model or data-property

The property comes from wire:model on the select, or from data-property. When both are present, data-property wins.

1<select data-pharaonic="select2" wire:model="country">...</select>
2<select data-pharaonic="select2" data-property="address.country_id">...</select>

Both accept a property name or a dot path (address.city, rows.2.product).

#Modifiers on Livewire 3

The modifiers mean what they mean in Livewire 3:

AttributeThe choice is sent
wire:modelwith the component's next request, such as a wire:click or a form submit
wire:model.liveright away
wire:model.change, wire:model.lazy, wire:model.blurright away: a Select2 choice is the change, and Select2 never blurs the native select
wire:model.debounce.500ms (without .live)with the next request, like wire:model

Use wire:model.live when the server has to react to the choice at once, for example to load dependent options. data-property always sends right away.

#How Values Move

  • Initial value. When the select mounts, it shows the property's current value.
  • User to server. A Select2 change sends the new value to the property in a Livewire request.
  • Server to widget. When a Livewire update changes the property, from an action, a lifecycle hook, or another field, the widget selects the new value.
  • No echo. Values the server sets are shown without being sent back, and a choice equal to the current value sends nothing.

#Value Types

Select2 works with strings. The package converts at the boundary:

Property valueSingle select showsMultiple select shows
nullthe empty optionnothing
''the empty optionnothing
0 or '0'the option with value 0
2 or '2'the option with value 2
['1', 2]options 1 and 2

What the user picks is sent as a string, or an array of strings for a multiple select. Cast it in the component if you need integers.

#Tags

With data-tags, users can type new values. Values in the property that have no <option>, such as tags saved earlier, get one when they are shown, so they appear on the first render.

1<div wire:ignore>
2 <select multiple data-pharaonic="select2" wire:model="labels" data-tags>
3 <option value="urgent">urgent</option>
4 </select>
5</div>

#Reacting to Changes

wire:change on the select runs after the new value is written, so the action sees it:

1<div wire:ignore>
2 <select data-pharaonic="select2" wire:model="country" wire:change="countryChanged">...</select>
3</div>

Select2 itself never fires the native change event; the package fires one for selects that have wire:change. With plain wire:model, the deferred value travels in the wire:change request. Change handlers you attach with jQuery receive that event too, so on these selects they run twice. An updated hook in the component needs no event at all:

app/Livewire/Order.php
1public function updatedCountry($value)
2{
3 $this->city = null;
4}

#Options

There are three ways to configure Select2, from the simplest:

  1. the package's shorthand attributes, for the most common options;
  2. Select2's own data-* attributes;
  3. data-options, a JSON object, for everything else.

#Shorthand Attributes

1<select data-pharaonic="select2" wire:model="country" data-placeholder="Choose a country" data-clear data-search-off>
AttributeSelect2 option
data-tagstags: true: users can type new values
data-clearallowClear: true: shows a clear button
data-search-offminimumResultsForSearch: -1: hides the search field
data-parent="#selector"dropdownParent: opens the dropdown inside that element
data-parent (no value)dropdownParent: the closest modal or dialog
data-template="name"templateResult and templateSelection: a function defined with define() (see Templates and Callbacks)

A shorthand is on when the attribute is present, with or without a value. data-clear="false" turns it off, which helps when Blade decides: data-clear="{{ $nullable ? 'true' : 'false' }}".

Select2 shows the clear button only when there is a placeholder. When you turn data-clear on without a placeholder, the package sets an empty one, so the button appears without adding text.

Use data-parent for selects inside modals, so the dropdown opens inside the modal and its search field can take focus. Without a value, it picks the closest .modal (Bootstrap), <dialog>, or [role="dialog"]:

1<div class="modal" id="edit-modal">
2 <div wire:ignore>
3 <select data-pharaonic="select2" wire:model="type" data-parent>...</select>
4 </div>
5</div>

With a CSS selector, it uses that element instead: data-parent="#edit-modal". When nothing matches, the dropdown opens in body as usual.

#Select2's Own data-* Attributes

Select2 reads any option from a data-* attribute, with the option name in kebab case. These need no help from the package:

AttributeSelect2 option
data-placeholder="Choose"placeholder
data-width="100%"width
data-language="ar"language
data-dir="rtl"dir
data-theme="bootstrap-5"theme (load the theme's stylesheet yourself)
data-minimum-input-length="2"minimumInputLength
data-maximum-selection-length="3"maximumSelectionLength
data-ajax--url="/api/countries"ajax.url
Booleans in Select2's attributes

Select2 reads its data-* attributes as plain strings, so any non-empty value counts as true: data-close-on-select="false" keeps closing the dropdown. Set booleans that have no shorthand in data-options.

#data-options

Anything else goes in data-options as a JSON object:

1<select data-pharaonic="select2" wire:model="labels" multiple data-options='{"closeOnSelect": false, "tokenSeparators": [","]}'>

Use single quotes around the attribute so the JSON can use double quotes. In Blade you can build it from PHP:

1<select data-pharaonic="select2" wire:model="country" data-options='@json(["placeholder" => __("Choose a country"), "allowClear" => true])'>

data-options can also hold dropdownParent as a selector. Invalid JSON, or JSON that is not an object, is reported as invalid-options and the select is not mounted.

#Which Wins

data-options overrides the shorthand attributes. Select2's own data-* attributes override both, because Select2 applies them last. Set each option in one place.

#Functions in Options

Some Select2 options are JavaScript functions, which attributes cannot hold. Define the function once in JavaScript and use its name in data-options. See Templates and Callbacks.

#Changing Options

Inside wire:ignore, Livewire never changes the select, so new attributes from the server are not applied. To change options or the list of <option> elements, let Livewire replace the block with a new wire:key. See Dynamic Selects.

#Templates and Callbacks

#Images Without JavaScript

Add data-image to an <option> to show an image before its text, in the dropdown and in the selection:

1<div wire:ignore>
2 <select data-pharaonic="select2" wire:model="country">
3 @foreach ($countries as $country)
4 <option value="{{ $country->id }}" data-image="{{ asset('flags/'.$country->code.'.svg') }}">{{ $country->name }}</option>
5 @endforeach
6 </select>
7</div>

The image is 1.25em high, aligned with the text, and has the pharaonic-select2-image class for your own styling. Options without data-image show their text as usual. A select with its own templateResult or templateSelection uses those instead.

#Your Own Templates

Select2 options such as templateResult take functions. Define a function with a name, then use the name in data-template or data-options.

#define(name, callback)

resources/js/app.js
1Pharaonic.select2.define('flag', (state) => {
2 if (!state.element) {
3 return state.text
4 }
5 
6 const span = document.createElement('span')
7 
8 span.textContent = `${state.element.dataset.flag} ${state.text}`
9 
10 return span
11})
resources/views/livewire/profile.blade.php
1<div wire:ignore>
2 <select data-pharaonic="select2" wire:model="country" data-template="flag">
3 <option value="eg" data-flag="🇪🇬">Egypt</option>
4 <option value="jo" data-flag="🇯🇴">Jordan</option>
5 </select>
6</div>

data-template sets both templateResult and templateSelection. To use different functions, name them in data-options, which overrides data-template:

1<select data-pharaonic="select2" wire:model="country" data-template="flag" data-options='{"templateSelection": "flagOnly"}'>
  • A name starts with a letter or digit and continues with letters, digits, ., :, _ or -. Anything else, or a callback that is not a function, throws an error with the code invalid-function.
  • Defining the same function under the same name again does nothing. A different function under a taken name throws function-conflict.
  • With a bundler, import define from the ES module instead of using window.Pharaonic.select2.

#Options That Accept a Name

OptionOption
templateResultcreateTag
templateSelectioninsertTag
matchertokenizer
sorterajax.data
escapeMarkupajax.processResults
ajax.transport

Any other option keeps its JSON value. ajax.url stays a string.

#Load Order

Call define after @livewireSelect2Scripts has loaded, for example in a deferred or module script, or import it from the ES module. A select that uses a name that is not defined yet waits: a console warning names the function once, and the select mounts as soon as define is called.

With wire:navigate, Livewire runs every <script> in the new page's body again. A script that calls define again creates a new function under the same name, which throws function-conflict. Put such scripts in <head>, which Livewire merges instead of running again, give them data-navigate-once, or keep them in your bundle.

#Remote Data

resources/js/app.js
1Pharaonic.select2.define('countries', (data) => ({
2 results: data.map((country) => ({ id: country.id, text: country.name })),
3}))
1<select data-pharaonic="select2" wire:model="country_id" data-options='{"ajax": {"url": "/api/countries", "dataType": "json", "processResults": "countries"}}'>
2 @if ($country)
3 <option value="{{ $country->id }}" selected>{{ $country->name }}</option>
4 @endif
5</select>

With remote data, Select2 can only show a value that has an <option>. Render the selected one in Blade, as above.

Build markup safely

Select2 escapes strings returned by templates. To render markup, return DOM nodes or a jQuery object built with textContent or text(), as in the examples. Never build HTML strings from option text or user data.

#Dynamic Selects

Livewire adds, removes, and re-renders elements. The package follows it.

#Selects Livewire Adds or Removes

Selects that appear after a Livewire update, such as a new row from wire:click="addRow", mount and bind after that update. Selects that disappear are cleaned up: their handlers are removed and Select2 is destroyed.

resources/views/livewire/invoice.blade.php
1@foreach ($rows as $index => $row)
2 <div wire:key="row-{{ $row['id'] }}">
3 <div wire:ignore>
4 <select data-pharaonic="select2" wire:model.live="rows.{{ $index }}.product">...</select>
5 </div>
6 </div>
7@endforeach
8 
9<button wire:click="addRow">Add row</button>
Key rows by something stable

Give each row a wire:key that stays with the row, such as an id, not its position. When a row in the middle is removed, position keys shift, and Livewire keeps the wrong widgets.

#Dependent Selects

When one select decides the options of another, the second select needs its new <option> list from the server. There are two ways.

A new wire:key per parent value. Livewire replaces the ignored block when its key changes. The old widget is cleaned up and the new select mounts with the new options.

1<div wire:ignore>
2 <select data-pharaonic="select2" wire:model.live="level">...</select>
3</div>
4 
5<div wire:ignore wire:key="group-{{ $level }}">
6 <select data-pharaonic="select2" wire:model.live="group">
7 <option value="">Choose a group</option>
8 @foreach ($groups as $id => $name)
9 <option value="{{ $id }}">{{ $name }}</option>
10 @endforeach
11 </select>
12</div>

No wire:ignore. Livewire updates the select, and the package rebuilds the widget after each update.

1<div>
2 <select data-pharaonic="select2" wire:model.live="group">...</select>
3</div>

The parent select needs wire:model.live: with plain wire:model, the server learns the parent value only on the next request, so the dependent options would not change.

The first way keeps the widget untouched by unrelated requests. The second is simpler, but every request that re-renders the component rebuilds the widget and closes an open dropdown.

Reset the child property when the parent changes:

app/Livewire/Enrollment.php
1public function updatedLevel()
2{
3 $this->group = '';
4}
Key ignored blocks that come and go

Without keys, Livewire compares siblings by position. When ignored blocks can appear or disappear next to each other, give each a unique wire:key.

#Nested Components and Many Selects

Each select binds to the closest Livewire component around it. A select in a nested component writes to that component, never to the parent. Any number of selects can share a page.

data-component-id binds a select to a specific component wherever it is. In a Livewire 3 view the id is $this->getId(); $this->id does not exist on Livewire 3 components and fails the page. An empty value means the closest component.

#Selects You Add Yourself

Selects you add with JavaScript, outside a Livewire update, need one call:

1Pharaonic.assistant.scan(container) // mounts the selects in container
2Pharaonic.assistant.destroy(container) // cleans them up

#wire:navigate

Widgets are cleaned up just before wire:navigate leaves a page, so the page kept for the back button holds plain selects, and they are mounted again when a page is shown, through a link, back, or forward. The package's script tags carry data-navigate-once, so they run once.

#Upgrading from 1.2

Releases 1.0 to 1.2 were built for Livewire 2. On Livewire 3, this 3.x line replaces them. Your markup keeps working, with two Livewire 3 changes below.

#Install 3.x

A ^1.2 constraint never moves to 3.x, so require the new line:

Terminal
composer require pharaonic/livewire-select2:^3.0
1.23.x
PHP7.2+8.1 to 8.5
Livewire2.0+3.8.3+
Laravelthrough Livewire10.15+, 11, 12, 13

Composer also installs pharaonic/livewire-assistant.

#Two Livewire 3 Changes

$this->id fails on Livewire 3. Livewire 3 components have no id property, so data-component-id="{{ $this->id }}" throws "Property [$id] not found on component" and the page fails. Remove the attribute, because each select binds to the component around it, or write {{ $this->getId() }}.

Plain wire:model is deferred. On Livewire 3, wire:model sends the choice with the component's next request, and wire:model.live sends it right away. Add .live where the server must react at once, such as a select that loads dependent options. wire:model.defer is not a Livewire 3 modifier; it behaves like plain wire:model.

#Scripts

Use the directive in new code:

resources/views/layouts/app.blade.php
1{{-- 1.2 --}}
2<x-pharaonic-select2::scripts />
3 
4{{-- 3.x --}}
5@livewireSelect2Scripts

<x-pharaonic-select2::scripts /> renders exactly the same tags, so you can switch whenever you like.

If you published vendor/pharaonic/pharaonic.select2.min.js and load it with your own <script> tag, switch to the directive now: the file needs the Livewire Assistant runtime, which the directive loads. Then delete the published file.

Deprecated, not removed

The scripts component, the publish tag, and the old JavaScript functions still work on every 3.x release. New code should use their replacements. They will be removed no earlier than a future major line.

#Attributes

1.23.x
data-component-id="{{ $this->id }}"Optional; {{ $this->getId() }} when you need it
wire:modelDeferred on Livewire 3; wire:model.live sends right away
data-tags, data-clear, data-search-off, data-parentUnchanged; "false" now turns one off, and data-parent without a value means the closest modal
data-placeholder, data-language, data-dirUnchanged: Select2 reads them itself

New: data-options for any other Select2 option, data-template and Pharaonic.select2.define() for templates, and data-image on options. See Options and Templates and Callbacks.

#Behaviour Changes

  • Server-side changes to the property now show in the widget.
  • Selects that Livewire adds later mount by themselves; you no longer need to dispatch pharaonic.select2.init.
  • wire:change on the select now runs.
  • A select without wire:model no longer throws.
  • Widgets work across wire:navigate visits.
  • The script no longer needs a global $; it reads window.jQuery.
  • A select without a class no longer gets form-select. Add the class yourself if your theme needs it.
  • No placeholder is set unless you give one; 1.2 used "Select an option".
  • Language and direction are no longer forced to en and ltr. Select2 takes them from the page (lang and dir) unless you set data-language or data-dir.

#JavaScript

Pharaonic.Select2.init(), Pharaonic.Select2.load(), and the pharaonic.select2.init and pharaonic.select2.load events still work: they call Pharaonic.assistant.scan(). In new code, call that directly for selects you add yourself.

#API Reference

#Markup

AttributeValue
data-pharaonicselect2
wire:modelProperty name or dot path; deferred unless .live, .change, .lazy, or .blur
data-propertyProperty name or dot path; wins over wire:model
data-tagstags: true; "false" turns it off
data-clearallowClear: true, with an empty placeholder when none is set; "false" turns it off
data-search-offminimumResultsForSearch: -1; "false" turns it off
data-parentdropdownParent from a CSS selector; without a value, the closest .modal, <dialog>, or [role="dialog"]
data-templateNamed function for templateResult and templateSelection
wire:changeRuns after the new value is written
data-optionsJSON object passed to Select2; overrides the shorthands
data-component-idComponent id ($this->getId()); empty or absent means the closest component

On <option>:

AttributeValue
data-imageImage URL shown before the option text

#Browser API

Available as window.Pharaonic.select2 and as named exports of dist/livewire-select2.esm.js.

MethodDescriptionReturns
define(name, callback)Names a function for function options in data-optionsundefined

Deprecated aliases, on window.Pharaonic.Select2:

MethodDescription
init()Calls Pharaonic.assistant.scan(document)
load(element)Calls Pharaonic.assistant.scan(element); accepts an element or a jQuery object

#Events

EventDirectionPurpose
pharaonic:assistant:errordispatched on windowFailures while mounting, updating, or cleaning up (see the Livewire Assistant diagnostics)
pharaonic.select2.initlistened on windowDeprecated: scans the document
pharaonic.select2.loadlistened on windowDeprecated: scans event.detail.target

#Error Codes

CodeRaised whenChannel
invalid-functiondefine() got an invalid name or a callback that is not a functionthrown
function-conflictdefine() got a different function for a taken namethrown
select2-missingjQuery or Select2 is not on window.jQuery once the page has loadedcause of mount-failed

#Laravel

NamePurpose
@livewireSelect2Scripts / @livewireSelect2Scripts(['nonce' => $nonce])Renders the Assistant runtime and the integration once per page
pharaonic.livewire-select2.scriptRoute name of GET /pharaonic/livewire-select2/select2.js
Pharaonic\Livewire\Select2\Select2ServiceProviderAuto-discovered service provider
<x-pharaonic-select2::scripts />Deprecated: renders @livewireSelect2Scripts
livewire-select2 publish tagDeprecated: copies the script to public/vendor/pharaonic/pharaonic.select2.min.js

#Compatibility

Every combination below installs with Composer and passes PHPStan and the PHP suite with lowest and highest dependencies. The real-browser suite (real Livewire 3 components, the real Livewire Assistant runtime, jQuery, and Select2, driven in real browsers) runs on the lowest PHP of each Laravel version, with lowest and highest dependencies.

LineLivewireLaravelPHPBrowsers
3.x3.8.3+10.15+8.1, 8.2, 8.3Chromium, Firefox, WebKit
3.x3.8.3+118.2, 8.3, 8.4Chromium, Firefox, WebKit
3.x3.8.3+128.2, 8.3, 8.4, 8.5Chromium, Firefox, WebKit
3.x3.8.3+138.3, 8.4, 8.5Chromium, Firefox, WebKit

The browser suite uses jQuery 4 and Select2 4.1. Laravel 10 starts at 10.15, because Livewire 3.8.3 renders its scripts with Stringable::isUrl(), which earlier Laravel 10 releases do not have.

#Why Livewire 3.8.3

Livewire 3.8.2 and earlier are affected by CVE-2026-81887 (DOM-based cross-site scripting during client-side state handling), and earlier releases by CVE-2025-54068 (remote command execution during property hydration) and CVE-2024-47823. Requiring 3.8.3 keeps every known Livewire advisory out of applications that install this line, and every Livewire 3 application can update to it within its major.

Unmaintained Laravel versions

Laravel 10 and 11 no longer receive security fixes. This line supports them because Livewire 3 does, but your application still carries their advisories. Plan your upgrade to Laravel 12 or 13.

#Examples

#1. Edit Form in a Modal

The server fills the form when the modal opens; the select shows the saved value and opens its dropdown inside the modal.

app/Livewire/Items.php
1<?php
2 
3namespace App\Livewire;
4 
5use App\Models\Category;
6use App\Models\Item;
7use Livewire\Component;
8 
9class Items extends Component
10{
11 public $editing = false;
12 
13 public $itemId;
14 
15 public $category_id;
16 
17 public function edit($id)
18 {
19 $item = Item::findOrFail($id);
20 
21 $this->itemId = $item->id;
22 $this->category_id = $item->category_id;
23 $this->editing = true;
24 }
25 
26 public function render()
27 {
28 return view('livewire.items', ['categories' => Category::pluck('name', 'id')]);
29 }
30}
resources/views/livewire/items.blade.php
1<div>
2 <div class="modal" style="display: {{ $editing ? 'block' : 'none' }}">
3 <div wire:ignore>
4 <select data-pharaonic="select2" wire:model="category_id" data-placeholder="Category" data-clear data-parent>
5 <option value=""></option>
6 @foreach ($categories as $id => $name)
7 <option value="{{ $id }}">{{ $name }}</option>
8 @endforeach
9 </select>
10 </div>
11 </div>
12</div>

#2. Country and City

The city list depends on the country. The country select uses wire:model.live, so the server re-renders the cities at once, and a new wire:key replaces the city select whenever the country changes.

app/Livewire/Address.php
1public $country_id = '';
2 
3public $city_id = '';
4 
5public function updatedCountryId()
6{
7 $this->city_id = '';
8}
9 
10public function render()
11{
12 return view('livewire.address', [
13 'countries' => Country::pluck('name', 'id'),
14 'cities' => City::where('country_id', $this->country_id)->pluck('name', 'id'),
15 ]);
16}
resources/views/livewire/address.blade.php
1<div>
2 <div wire:ignore>
3 <select data-pharaonic="select2" wire:model.live="country_id">
4 <option value="">Country</option>
5 @foreach ($countries as $id => $name)
6 <option value="{{ $id }}">{{ $name }}</option>
7 @endforeach
8 </select>
9 </div>
10 
11 <div wire:ignore wire:key="cities-{{ $country_id }}">
12 <select data-pharaonic="select2" wire:model="city_id">
13 <option value="">City</option>
14 @foreach ($cities as $id => $name)
15 <option value="{{ $id }}">{{ $name }}</option>
16 @endforeach
17 </select>
18 </div>
19</div>

#3. Invoice Rows

Each row has its own select bound to its own array item.

resources/views/livewire/invoice.blade.php
1<div>
2 @foreach ($rows as $index => $row)
3 <div wire:key="row-{{ $row['id'] }}">
4 <div wire:ignore>
5 <select data-pharaonic="select2" wire:model="rows.{{ $index }}.product_id">
6 <option value="">Product</option>
7 @foreach ($products as $id => $name)
8 <option value="{{ $id }}">{{ $name }}</option>
9 @endforeach
10 </select>
11 </div>
12 <button wire:click="removeRow({{ $index }})">Remove</button>
13 </div>
14 @endforeach
15 
16 <button wire:click="addRow">Add row</button>
17</div>

#4. Tags

Users pick existing tags or type new ones; saved tags show on load even without an option.

resources/views/livewire/post-form.blade.php
1<div wire:ignore>
2 <select multiple data-pharaonic="select2" wire:model="tags" data-tags data-options='{"tokenSeparators": [","]}'>
3 @foreach ($suggestions as $tag)
4 <option value="{{ $tag }}">{{ $tag }}</option>
5 @endforeach
6 </select>
7</div>

#Troubleshooting

#"Property [$id] not found on component"

The view uses {{ $this->id }}, which Livewire 3 components do not have. Remove data-component-id, because the select binds to the component around it anyway, or use {{ $this->getId() }}.

#The server gets the value only after I click something

Plain wire:model is deferred on Livewire 3: the value travels with the component's next request. Use wire:model.live to send it right away.

#A validation message does not show

The @error is inside the wire:ignore block, which Livewire never updates. Move it outside, next to the block. See Basic Usage.

#Nothing happens and there is no widget

Check the console. select2-missing means jQuery or Select2 is not on window.jQuery: load them, or with a bundler set window.jQuery and call select2() (see Installation). Make sure @livewireSelect2Scripts is on the page.

#"$ is not a function"

The package never uses $; it reads window.jQuery when a select mounts. The error comes from code that expects a global $. With a bundler, set window.jQuery as shown in Installation.

#The widget disappears or resets after a request

The select is outside wire:ignore and the package rebuilds it after each update, or Livewire replaced the block. Wrap the select in wire:ignore unless you want the server to change its options.

#New options from the server do not appear

Inside wire:ignore, Livewire never changes the select. Give the ignored block a wire:key that changes with the data, or remove wire:ignore. See Dependent Selects.

#My jQuery change handler runs twice

The select has wire:change. Select2 fires its own jQuery change event, and the package fires a native one so wire:change runs. Use wire:change or the updated hook instead of the jQuery handler, or remove wire:change.

#The value is a string, but my property is an integer

Select2 works with strings. Cast in the component, for example in an updated hook.

#The dropdown opens behind a modal, or its search field cannot take focus

Add data-parent to the select. It opens the dropdown in the closest .modal, <dialog>, or [role="dialog"]; give it a selector when the modal is something else.

#A select in a nested component updates the parent

Remove a data-component-id that names the parent, or leave the attribute out: the closest component is used.

#Composer refuses to install or update Laravel 10 or 11

Those releases have unfixed security advisories. See the warning in Installation.

Contributors

Amazing people who made this package possible

This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Select2!

Moamen Eltouny

@MoamenEltouny

43 contributions

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.