<div
class="container"
x-data=`autocomplete({})`
@click.outside="open=false">
<div x-show="limit > 1" class="selections">
<template x-for="item in selected">
<div>
<span x-text="item.value"></span>
<button type="button" @click="remove(item)" class="remove">x</button>
</div>
</template>
<div x-show="selected?.length > 1" class="remove-all">
<button type="button" @click="removeAll">Remove all</button>
</div>
</div>
<div class="input-container" @click="open=true">
<input
x-model="query"
x-ref="queryinput"
:placeholder="placeholder"
class="field"
@input.debounce.200ms="fetchOptions"
/>
<input type="hidden" :name="name" x-model="formVal" />
<button
x-show="selected.length > 0 && limit === 1"
type="button"
class="remove"
@click="limit===1 ? removeAll(): clear()"
>x
</button>
</div>
<div x-show="open" class="options">
<template x-for="option in options">
<button
x-text="option.value"
type="button"
@click="select(option); $refs.queryinput.focus()"
class="option">
</button>
</template>
</div>
</div>
document.addEventListener("alpine:init", () => {
Alpine.data(
"autocomplete",
function ({
getResults = async query => {
// Fetch the results here, format them as [{key: obj.keyField, value: obj.valueField}, ....] and return
const fruits = [
"apple",
"banana",
"orange",
"mango",
"pineapple",
"papaya",
"watermelon",
"pomegranate"
]
return fruits
.filter(item => item.includes(query))
.map(item => ({key: item, value: item}))
},
// Max number of selections that can be made. Set it to 1 to use it as single select
limit = 5,
placeholder = "Search for a fruit",
// Set selected=[{key: "apple", value: "apple"}] to have apple selected by default
selected = [],
// Max number of results that can be shown in the dropdown
maxResults = 5
}) {
return {
limit,
placeholder,
selected,
options: [],
query: "",
open: false,
init() {
if (this.limit === 1) this.query = this.selected?.[0]?.value
},
formVal() {
return this.selected.map(obj => obj.key).join("$")
},
async fetchOptions() {
if (this.open === false) this.open = true
if (this.query.length < 1) this.options = []
this.options = (await getResults(this.query)).slice(0, maxResults)
},
clear() {
this.query = ""
},
select(item) {
if (!this.selected.find(obj => obj.key === item.key))
this.selected = [...this.selected.slice(0, this.limit - 1), item]
this.open = false
if (this.limit === 1) {
this.query = this.selected?.[0]?.value
} else {
this.clear()
}
this.$dispatch("change")
},
remove(item) {
this.selected = this.selected.filter(obj => obj.key !== item.key)
this.$dispatch("change")
},
removeAll() {
this.selected = []
this.clear()
this.$dispatch("change")
}
}
}
)
})
.container {
@apply relative w-full;
}
.selections {
@apply flex flex-wrap gap-2 mb-1;
}
.selections > div {
@apply flex items-center gap-1 border whitespace-nowrap rounded-full px-2 py-0.5 text-sm border-blue-500;
}
.selections .remove {
@apply pl-2 pr-1 text-base border-l ml-1;
}
.selections .remove-all {
@apply flex items-center gap-1 border whitespace-nowrap rounded-full px-2 py-0.5 text-sm border-blue-500 bg-red-500 text-white;
}
.input-container {
@apply flex h-10 border border-yellow-800;
}
.input-container .field {
@apply border-none outline-none pl-2 bg-yellow-100 w-full;
}
.input-container .remove {
@apply bg-yellow-100 border-l px-3 text-xl;
}
.options {
@apply absolute shadow top-[100%] z-40 w-full bg-white flex flex-col;
}
.options .option {
@apply py-2 text-left px-4 hover:bg-yellow-400 focus:bg-yellow-400;
}