Skip to content

Instantly share code, notes, and snippets.

@CallumCarmicheal
Forked from mithicher/tom-select.blade.php
Last active February 25, 2026 00:05
Show Gist options
  • Select an option

  • Save CallumCarmicheal/3bcbfb178443c9a11c673be83530ac8d to your computer and use it in GitHub Desktop.

Select an option

Save CallumCarmicheal/3bcbfb178443c9a11c673be83530ac8d to your computer and use it in GitHub Desktop.
Tom Select Livewire Blade Component (Real time with changing item list)

Attributions

This is a heavily modified version of the original code by mithicher. The previous fork only worked one way so once the component was rendered the options list could not be changed.

If you have any improved versions or better approaches to this, please leave a comment and i'll add it up here. :)

Version changes / Edits

Version 1 - Add code and descriptions.

Version 2 - Rewrite the code to have the script inside the alpine js x-init attribute, this was required because issues with addEventListener('load', ...) not triggering when ever the dropdown is loaded into the window after the initial state (behind a if / else branch).

Version 3 - Updated attributions, I used the wrong gist.

Attributes / Parameters

:options - This is an array of the format [ 'id' => 'any', 'title' => 'string', 'subtitle' => 'string' ] for the items in the select dropdown

:selectedItems - A string array of the selected items id's, this can be used in-place of x-model, kept over from the original implementation although I haven't really found a use for it.

id - required if multiple of the same wire:model binding is used

wire:model - The binding source for livewire

multiple - Allow multiple selection of items, returns an array of strings into wire:modal.

Rendering / Item layout

The item layout by default is [ 'id' => 'any', 'title' => 'string', 'subtitle' => 'string' ]. These can be modified by changing the renderTemplate in the x-data attribute of tom-select.blade.php file.

id in each item needs to be unique, tom select will not render items with the same id.

Usage

To enable real-time tracking of the model value's and item list we need to keep track of the dom, because of this we need an id for our element. By default we are using the wire:modal attribute to create the id and reference that. ($modelId, $attrId and $compId - element id used).

Code Examples

Rendering a array inline

<x-tom-select class="form-control mb-3"
  wire:model="selectedCountry"
  :options="[
    [ 'id' => 'ENGLAND',  'title' => 'England', 'subtitle' => 'Population - 57,690,300' ],
    [ 'id' => 'WALES',    'title' => 'Wales', 'subtitle' => 'Population - 3,164,400' ],
    [ 'id' => 'SCOTLAND', 'title' => 'Scotland', 'subtitle' => 'Population - 5,490,100' ],
    [ 'id' => 'NIRELAND', 'title' => 'North Ireland', 'subtitle' => 'Population - 1,920,400' ],
  ]"
/>

Rendering a eloquent collection

<x-tom-select class="form-control mb-3"
  wire:model="form.clientTypeCategory"
  :options="collect( \App\Models\Users::all() )
            ->map(fn ($item) => [ 'id' => $item->id, 'title' => $item->username, 
                                  'subtitle' => $item->email ])"
/>

Using a eloquent model, (conversion in model)

<x-tom-select class="form-control mb-3"
  wire:model="form.clientTypeCategory"
  :options="\App\Models\Mattersphere\CodeLookup::SelectInputLookup('uCOMCLITYPE')"
/>

<?php
// file: /App/Models/Mattersphere/CodeLookup.php
public static function SelectInputLookup($cdType) {
    return collect( self::where('cdType', $cdType)->all() )
        ->map(
            fn ($item) => [ 'id' => $item->cdCode, 'title' => $item->cdDesc ]
        )->toArray();
}

Rendering a filtered collection

<x-tom-select class="form-control" multiple
  wire:model="form.clientWorkTypes"
  :options="collect( \App\Models\Mattersphere\CodeLookup::SelectInputLookup('FILETYPE') )
          ->filter(fn($ft) => $ft['id'] != 'TEMPLATE')->values()->all()"
/>

Using a filtering a dropdown based on another dropdown

<label>Please select a user:</label>
<x-tom-select class="form-control mb-3" wire:model="selectUserId"
  :options="collect( \App\Models\Users::all() )
            ->map(fn ($item) => [ 'id' => $item->id, 'title' => $item->username, 
                                  'subtitle' => $item->email ])"
/>

<label>Please select a transaction:</label>
<x-tom-select class="form-control mb-3" wire:model="selectedTransactionId"
  :options="collect( \App\Models\Transaction::where('user_id', '=', selectUserId)->all() )
            ->map(fn ($item) => [ 'id' => $item->id, 'title' => $item->username, 
                                  'subtitle' => $item->email ])"
/>
@props([
// item format: [ 'id' => '', 'title' => '', 'subtitle' => '' ]
'options' => [],
// optional, only use if wire:model is not functioning
// correctly: ['ITEM1','ITEM2','ITEM3']
'selectedItems' => [],
'optGroups' => [],
'optgroupField' => 'group'
])
{{--
Github docs: https://gist.github.com/CallumCarmicheal/3bcbfb178443c9a11c673be83530ac8d
--}}
@php
// Get the wire model and convert it into a unique id
$modelId = $attributes->wire('model');
if ($modelId != null) $modelId = str_replace('\'', '', str_replace('\"', '', $modelId));
// Get the id, attribute
$attrId = $attributes->get('id') ?? null;
if ($attrId != null) $attrId = str_replace('\'', '', str_replace('\"', '', $attrId));
// Attempt to create a unique component id.
$compId = (!isset($attrId) || trim($attrId) === '') ? str_replace('.', '_', $modelId) : $attrId;
@endphp
<!-- x-tom-select container -->
<div id="{{$compId}}_container">
<!-- DOM Attribute tracking to get latest value of $options if it changes. -->
<div id="{{$compId}}_dropdownItems" class="d-none" x-items="{{ collect($options)->toJson() }}"></div>
<!-- Alpine js binding to flag an event to trigger when ever the current value changes,
if this is not done then the value will not reflect from any server side changes. -->
<div wire:ignore {{ $attributes->wire('model') }}
class="d-none" x-data="{ value: null }" x-modelable="value"
x-init="
$(function() {
console.log('x-init . jq$startup -> {{$compId}}');
//console.log('[{{$compId}}] Loading from script tags... Document ready state = ', document.readyState);
$('#{{$compId}}_container').attr('loaded', 'true');
// Create a DOM Observer to listen for changes on the item list.
function createObserver(targetElementSelector, attributeFilter=[], callback) {
//console.log('Started observing: ', targetElementSelector, 'Attr filter: ', attributeFilter);
let targetElement = document.querySelector(targetElementSelector);
if (targetElement) {
// Create a new instance of MutationObserver
const observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
//console.log('mutation: ', mutation.type);
// Check if the mutation type is 'childList' or 'attributes'
if (mutation.type === 'childList' || mutation.type === 'attributes') {
callback(targetElement);
}
}
});
// Configure the observer to watch for changes in child elements and character data
const config = {
childList: true, // Watch for the addition or removal of child nodes
subtree: true, // Watch for changes in all descendants
//characterData: true, // Watch for changes to the text content of the node
attributeFilter: attributeFilter
};
// Start observing the target element
observer.observe(targetElement, config);
} else {
console.error('Element with class ' + targetElementSelector + ' not found.');
}
}
// Listen for dropdown item list changes
createObserver('#{{$compId}}_dropdownItems', ['x-items'], (targetElement) => {
// Get the item and parse it into a object.
let attr = targetElement.getAttribute('x-items');
let js = JSON.parse(attr); //
// Get our tom-select object
let container = $('#{{$compId}}_container');
let tomSelect = container.find('select')[0].tomselect;
let value = tomSelect.getValue();
// Clear the properties, set the item list and change our value.
tomSelect.clear();
tomSelect.clearOptions();
tomSelect.addOptions(js);
tomSelect.setValue(value, true); // silent: true (don't trigger $wire.$refresh())
});
});
$watch('value', (v) => {
let tomSelect = $('#{{$compId}}_container').find('select')[0].tomselect;
if (tomSelect.getValue() !== v) {
// Value is different on the server, update our drop-down.
//console.log('[{{$compId}}] Different value: ', tomSelect.getValue(), v);
// Try to delay it for issue with loading information.
setTimeout(() => {
// silent: true (don't trigger $wire.$refresh())
tomSelect.setValue(v, true);
}, 100);
} else {
// Value is the same, no change required.
//console.log('[{{$compId}}] value same: ', tomSelect.getValue(), v);
}
})
">
</div>
<!-- All changes in this element are ignored by wire, hence the hacky code above. -->
<div wire:ignore wire:loading.attr="disabled">
<select
id="{{$compId}}_select"
name="{{$attributes->get('name') ?? ($attributes->get('wire:model'))}}"
placeholder="(not selected)"
x-ref="input"
x-cloak
{{ $attributes->except(['name', 'id']) }}
x-data="{
tomSelectInstance: null,
options: {{ collect($options) }},
items: {{ collect($selectedItems) }},
optgroups: {{ collect($optGroups) }},
// Modify these functions if you wish to expand upon the
// template with your own variables.
//
// By default the following is implemented:
// [ 'id' => 'any', 'title' => 'string', 'subtitle' => 'string' ]
renderTemplate(data, escape) {
//console.log('x-tom-select: renderTemplate', data, this);
return `<div class='flex items-center'>
<div>
<div class='block font-medium text-gray-700'>${escape(data.title)}</div>
${data.subtitle == undefined ? '' : `<small class='block text-gray-500'>${escape(data.subtitle)}</small>`}
</div>
</div>`;
},
itemTemplate(data, escape) {
//console.log('x-tom-select: itemTemplate', data, this);
return `<div>
<span class='block font-medium text-gray-700'>${escape(data.title)}</span>
</div>`;
},
optHeaderTemplate(data, escape) {
//console.log('x-tom-select: optHeaderTemplate', data, this);
return `<div class='optgroup-header ${data.class == undefined ? '' : escape(data.class)}'>${escape(data.label)}</div>`;
}
}"
x-init="tomSelectInstance = new TomSelect($refs.input, {
valueField: 'id',
labelField: 'title',
searchField: ['title', 'subtitle'],
optgroupField: '{{$optgroupField}}',
options: options,
items: items,
optgroups: optgroups,
@if (!empty($items) && !$attributes->has('multiple'))
placeholder: undefined,
@endif
render: {
option: renderTemplate,
item: itemTemplate,
optgroup_header: optHeaderTemplate
},
maxOptions: null,
create: false,
hidePlaceholder: true,
plugins: {
'clear_button': {},
'caret_position': {}
},
onDropdownOpen: function(dropdown){
let bounding = dropdown.getBoundingClientRect();
if (bounding.bottom > (window.innerHeight || document.documentElement.clientHeight)) {
dropdown.classList.add('dropup');
}
},
onDropdownClose: function(dropdown){
dropdown.classList.remove('dropup');
}
});
tomSelectInstance.on('change', function() {
// Update our binding when we change item.
$wire.$refresh();
});"
></select>
</div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment