DEV Community

Jonathon Ringeisen
Jonathon Ringeisen

Posted on • Updated on

Building a Custom Select Input with Tailwind and Vue

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>
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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>
Enter fullscreen mode Exit fullscreen mode

If you have any improvement suggestions please comment below. I'd appreciate your feedback!

Top comments (8)

Collapse
 
clopezpro profile image
Christian López C • Edited

It helped me a lot, I added a search to the server and highlight
dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
eugenevdm profile image
Eugene van der Merwe • Edited

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.

Collapse
 
andy4277 profile image
andy4277

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'

Collapse
 
justageek profile image
Brian Smith

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?

Collapse
 
khalidedaig profile image
Khalid EDAIG

Thanks Bro for that

Collapse
 
justageek profile image
Brian Smith

Do you have a demo of this working so we can see it somewhere?

Collapse
 
jringeisen profile image
Jonathon Ringeisen
Collapse
 
aymenalhattami profile image
Ayman Alhattami

It does not work with me in vuejs 3