Created
September 28, 2022 04:47
-
-
Save codemonkey76/3657d36de46588cab6962c878874ac17 to your computer and use it in GitHub Desktop.
Searchable select component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Alpine from 'alpinejs' | |
import select from './components/select' | |
window.Alpine = Alpine | |
Alpine.data('select', select) | |
Alpine.start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@props([ | |
'keyField' => 'id', | |
'nameField' => 'name', | |
'secondaryField' => '', | |
'name' => '', | |
'options' => $options, | |
'label' => 'Select', | |
'id' => 'select' | |
]) | |
<div | |
x-data="select({ | |
options: {{ json_encode($options) }}, | |
name: '{{ $name }}', | |
value: @entangle($attributes->wire('model')), | |
keyField: '{{ $keyField }}', | |
nameField: '{{ $nameField }}', | |
secondaryField: '{{ $secondaryField }}', | |
id: '{{ $id }}', | |
label: '{{ $label }}' | |
})" | |
x-cloak | |
@click.away.stop="close()" | |
@keydown.esc="close()" | |
@keydown.arrow-up.prevent="focusPrevious()" | |
@keydown.arrow-down.prevent="focusNext()" | |
> | |
<label :for="id" class="block text-sm font-medium text-gray-700" x-text="label"></label> | |
<div class="relative mt-1"> | |
<input | |
@click="doSearch()" | |
x-show="!isSearching" | |
:value="selectedOptionName" | |
type="text" | |
class="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm" | |
readonly | |
> | |
<input x-ref="search" | |
x-show="isSearching" | |
x-model="search" | |
@keydown="doSearch()" | |
@click="toggle()" | |
class="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm" | |
> | |
<button @click="toggleOrClear()" type="button" | |
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"> | |
<svg x-show="hasSearch" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-gray-400"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
<svg x-show="!hasSearch" class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" | |
fill="currentColor" aria-hidden="true"> | |
<path fill-rule="evenodd" | |
d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" | |
clip-rule="evenodd"/> | |
</svg> | |
</button> | |
<ul | |
x-show="isOpen" | |
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> | |
<template x-for="option in filteredOptions"> | |
<li | |
class="relative cursor-default select-none py-2 pl-3 pr-9" | |
:class="{'text-white bg-indigo-600': focusedOption === option, 'text-gray-900': focusedOption !== option }" | |
@mouseenter="focusOption(option)" | |
@mouseleave="removeFocus()" | |
@click="selectOption(option)" | |
> | |
<div class="flex"> | |
<span | |
class="truncate" | |
:class="{'font-semibold': selectedOption === option}" | |
x-text="option[nameField]"></span> | |
<span | |
class="ml-2 truncate" | |
:class="{'text-indigo-200': focusedOption === option, 'text-gray-500': focusedOption !== option }" | |
x-text="option[secondaryField]"></span> | |
<span | |
x-show="selectedOption === option" | |
class="absolute inset-y-0 right-0 flex items-center pr-4" | |
:class="{'text-white': focusedOption === option, 'text-indigo-600': focusedOption !== option}"> | |
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" | |
fill="currentColor" | |
aria-hidden="true"> | |
<path fill-rule="evenodd" | |
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" | |
clip-rule="evenodd"/> | |
</svg> | |
</span> | |
</div> | |
</li> | |
</template> | |
</ul> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default (config) => ({ | |
keyField: config.keyField, | |
nameField: config.nameField, | |
secondaryField: config.secondaryField, | |
options: config.options, | |
id: config.id, | |
label: config.label, | |
isOpen: false, | |
isSearching: true, | |
focusedOption: null, | |
selectedOption: null, | |
search: '', | |
get filteredOptions() { | |
return this.options.filter( | |
i => i[this.nameField].toLowerCase().includes(this.search.toLowerCase()) | |
) | |
}, | |
get selectedOptionName() { | |
if (!this.selectedOption) return ''; | |
return this.selectedOption[this.nameField]; | |
}, | |
get hasSearch() { | |
return this.search !== '' | |
}, | |
close: function() { | |
this.isOpen = false | |
}, | |
open: function() { | |
this.isOpen = true | |
}, | |
doSearch: function() { | |
this.open() | |
this.search = this.selectedOption[this.nameField] | |
this.deselectOption() | |
}, | |
toggleOrClear: function() { | |
if (!this.hasSearch) return this.toggle() | |
this.search = '' | |
}, | |
toggle: function() { | |
this.isOpen = !this.isOpen | |
}, | |
removeFocus: function() { | |
this.focusedOption = null | |
}, | |
focusOption: function(option) { | |
this.focusedOption = option | |
}, | |
deselectOption: function() { | |
this.selectedOption = null | |
this.isSearching = true | |
this.$nextTick(() => this.$refs.search.focus()) | |
}, | |
selectOption: function(option) { | |
if (this.selectedOption === option) return this.deselectOption() | |
this.selectedOption = option | |
this.isSearching = false | |
this.close() | |
this.search = this.selectedOption[this.nameField] | |
} | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment