Skip to content

Instantly share code, notes, and snippets.

@CallumCarmicheal
Forked from mithicher/tom-select.blade.php
Last active November 9, 2024 12:05
Show Gist options
  • Save CallumCarmicheal/3bcbfb178443c9a11c673be83530ac8d to your computer and use it in GitHub Desktop.
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.

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' => []
])
{{--
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));
$compId = (!isset($attrId) || trim($attrId) === '') ? str_replace('.', '_', $modelId) : $attrId;
@endphp
<div id="{{$compId}}_container">
{{-- <p><i>{{$attrId}}</i> - <b>{{$modelId}}</b> - <small>{{$compId}}</small></p> --}}
{{-- <p>{{!isset($attrId) ? 't' : 'f' }} | {{ trim($attrId) === '' ? 't' : 'f' }} | {{(!isset($attrId) || trim($attrId) === '') ? 't' : 'f'}}</p> --}}
{{-- <p>{{ str_replace('.', '_', $modelId) }} = {{$compId}}</p> --}}
<!-- 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="$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('Different value: ', tomSelect.getValue(), v);
tomSelect.setValue(v, true); // silent: true (don't trigger $wire.$refresh())
} else {
// Value is the same, no change required.
//console.log('value same: ', tomSelect.getValue(), v);
}
})">
</div>
<script>
window.addEventListener('load', function() {
// 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())
});
});
</script>
<!-- All changes in this element are ignored by wire, hence the hacky code above. -->
<div wire:ignore>
<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) }},
renderTemplate(data, escape) {
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) {
return `<div>
<span class='block font-medium text-gray-700'>${escape(data.title)}</span>
</div>`;
}
}"
x-init="tomSelectInstance = new TomSelect($refs.input, {
valueField: 'id',
labelField: 'title',
searchField: 'title',
options: options,
items: items,
@if (!empty($items) && !$attributes->has('multiple'))
placeholder: undefined,
@endif
render: {
option: renderTemplate,
item: itemTemplate
},
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