JSFiddle - React, Tailwind, and code Playground

by Max Alex

HTML

<link rel="stylesheet" href="https://unpkg.com/bulma/css/bulma.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
  <autocomplete-input :options="options" @select="onOptionSelect">
    <template slot="item" scope="option">
      <article class="media">
        <figure class="media-left">
          <p class="image is-64x64">
            <img :src="option.thumbnail">
          </p>
        </figure>
        <p>
          <strong>{{ option.title }}</strong>
          <br> {{ option.description }}
        </p>
      </article>
    </template>
  </autocomplete-input>
  <div>
    <label>
      {{ this.option }}
    </label>
  </div>
</div>

<script id="autocomplete-input-template" type="text/x-template">
  <div class="autocomplete-input">
    <p class="control has-icon has-icon-right">
      <input v-model="keyword" class="input is-large" placeholder="Search..." @input="onInput($event.target.value)" @keyup.esc="closeDropdown" @blur="closeDropdown" @keydown.down="moveDown" @keydown.prevent.up="moveUp" @keydown.enter="select">
      <i class="fa fa-angle-down"></i>
    </p>
    <ul v-show="isOpen" class="options-list">
      <li :class="{
      'highlighted': highlightedPosition === 0
    }" @mouseenter="highlightedPosition = 0" @mousedown="select">
        Search for "{{ enteredValue }}"
      </li>

      <li v-for="(option, index) in fOptions" :class="{
          'highlighted': index === highlightedPosition - 1
        }" @mouseenter="highlightedPosition = index + 1" @mousedown="select">
        <slot name="item" :title="option.title" :description="option.description" :thumbnail="option.thumbnail">
      </li>
    </ul>
  </div>
</script>

CSS

html,
body {
  height: 100%;
}

#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  display: flex;
  justify-content: center;
  padding-top: 100px;
  height: 100%;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 10px;
}

input {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
}

.autocomplete-input {
  position: relative;
  height: 300px;
}

ul.options-list {
  display: flex;
  flex-direction: column;
  margin-top: -12px;
  border: 1px solid #dbdbdb;
  border-radius: 0 0 3px 3px;
  position: absolute;
  width: 100%;
  overflow: hidden;
}

ul.options-list li {
  width: 100%;
  flex-wrap: wrap;
  background: white;
  margin: 0;
  border-bottom: 1px solid #eee;
  color: #363636;
  padding: 7px;
  cursor: pointer;
}

ul.options-list li.highlighted {
  background: #f8f8f8
}

JavaScript

Vue.component('autocomplete-input', {
    template: '#autocomplete-input-template',
    props: {
      options: {
        type: Array,
        required: true
      }
    },
    data() {
      return {
        isOpen: false,
        highlightedPosition: 0,
        keyword: '',
        enteredValue: ''
      }
    },
    computed: {
      fOptions() {
        const re = new RegExp(this.enteredValue, 'i')
        return this.options.filter(o => o.title.match(re))
      }
    },
    methods: {
      onInput(value) {
          this.highlightedPosition = 0
          this.isOpen = !!value
          this.enteredValue = value
        },
        moveDown() {
          if (!this.isOpen) {
            return
          }
          this.highlightedPosition =
            (this.highlightedPosition + 1) % (this.fOptions.length + 1)
        },
        moveUp() {
          if (!this.isOpen) {
            return
          }
          this.highlightedPosition = this.highlightedPosition - 1 < 0 ? this.fOptions.length : this.highlightedPosition - 1
        },
        select() {
          const selectedOption = this.highlightedPosition === 0 ? {
            title: this.enteredValue
          } : this.fOptions[this.highlightedPosition - 1]
          this.$emit('select', selectedOption)
          this.isOpen = false
          this.keyword = this.enteredValue = selectedOption.title
        },
        closeDropdown() {
          this.keyword = this.enteredValue
          this.isOpen = false
        }
    },
    watch: {
      highlightedPosition() {
        this.keyword = this.highlightedPosition === 0 ? this.enteredValue : this.fOptions[this.highlightedPosition - 1].title
      }
    }
  })

  new Vue({
    el: '#app',
    data: {
      options: [{
        title: 'First Scene',
        description: 'lorem ipsum dolor amet.',
        thumbnail: 'http://lorempicsum.com/nemo/200/200/1'
      }, {
        title: 'Second Scene',
        description: 'lorem ipsum dolor amet.',
        thumbnail:...