Have you ever used Element UI? I am currently using this in a production application and realized that it's not very mobile-friendly, like at all! I'm using it for a couple of things like a select input with search functionality and a date/time picker. Both fail miserably on mobile devices and I found this out because my users started reporting it to me.
So I decided that I would build my own custom Vue components this way I can ensure that they're mobile friendly and I'll have more flexibility when it comes to customizing the component.
I decided to start with the AutoComplete component which I think is actually considered a select component.
The component looks like this:
<auto-complete
:data="data"
v-model.trim="formData.client"
@chosen="handleChosen"
placeholder="Search for state..."
></auto-complete>
My goal was to keep it simple but make it customizable so if anyone else wanted to use it they can customize it to their liking. The props include: placeholder
, data
, inputClass
, dropdownClass
.
I think I'm going to add some more to make it more customizable.
Alright, let's get to the good part, the code!
<template>
<div class="relative" v-click-outside="clickedOutside">
<input
:value="value"
@input="handleInput"
:placeholder="placeholder"
ref="input"
tabindex="0"
:class="inputClass"
/>
<span
v-if="value"
@click.prevent="reset()"
class="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer"
>
x
</span>
<div
v-show="value && showOptions"
@click.self="handleSelf()"
@focusout="showOptions = false"
tabindex="0"
:class="dropdownClass"
>
<ul class="py-1">
<li
v-for="(item, index) in searchResults"
:key="index"
@click="handleClick(item)"
class="px-3 py-2 cursor-pointer hover:bg-gray-200"
>
{{ item.name }}
</li>
<li v-if="!searchResults.length" class="px-3 py-2 text-center">
No Matching Results
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
required: false,
},
placeholder: {
type: String,
required: false,
default: "Enter text here.",
},
data: {
type: Array,
required: true,
},
inputClass: {
type: String,
required: false,
default:
"border border-gray-300 py-2 px-3 rounded-md focus:outline-none focus:shadow-outline",
},
dropdownClass: {
type: String,
required: false,
default:
"absolute w-full z-50 bg-white border border-gray-300 mt-1 mh-48 overflow-hidden overflow-y-scroll rounded-md shadow-md",
},
},
data() {
return {
showOptions: false,
chosenOption: "",
searchTerm: "",
};
},
computed: {
searchResults() {
return this.data.filter((item) => {
return item.name.toLowerCase().includes(this.searchTerm.toLowerCase());
});
},
},
methods: {
reset() {
this.$emit("input", "");
this.chosenOption = "";
},
handleInput(evt) {
this.$emit("input", evt.target.value);
this.searchTerm = evt.target.value;
this.showOptions = true;
},
handleClick(item) {
this.$emit("input", item.name);
this.$emit("chosen", item);
this.chosenOption = item.name;
this.showOptions = false;
this.$refs.input.focus();
},
clickedOutside() {
this.showOptions = false;
if (!this.chosenOption) {
this.$emit("input", "");
}
},
},
};
</script>
<style scoped>
.mh-48 {
max-height: 10rem;
}
</style>
If you have any improvement suggestions please comment below. I'd appreciate your feedback!
Top comments (8)
It helped me a lot, I added a search to the server and highlight
dev-to-uploads.s3.amazonaws.com/up...
Fantastic coding wonderful MVP for such a common select task. I'm trying to do something similar in Livewire but don't have the tailwind classes so I googled and found your article. Well done and keep up the great work.
This is really good, I used in Vue 3 so I had to change few things around like a $set to reactive wrapper.
One thing that I would suggest is to use computed property in your template instead of calling 'resultQuery()' directly. This way on any change your search will be run only once and not twice.
...
computed: {
searchResults: function() { return this.resultQuery(this.formData)}
}
....
template
...
resultQuery().length => searchResults.length
...
v-for="(value, index) in resultQuery()" to v-for='(value, index) in searchResults'
Thanks for the reply, how hard do you think it would be to refactor your tool so that it used an ajax request to return results based on the text entered, so say after a user types 3 letters you trigger the search via ajax and use the response to populate the v-for?
Thanks Bro for that
Do you have a demo of this working so we can see it somewhere?
I do, here codepen.io/jringeisen/pen/xxOzxYE.
It does not work with me in vuejs 3