Building a Custom Select Input with Tailwind and Vue
Tags: vue, tailwindcss, educational
Published: 2020-11-04 04:26:31

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:
```vue
<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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qd220sloovs1w4sn4o2d.png)

Alright, let's get to the good part, the code!

```vue
<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!