JSFiddle - React, Tailwind, and code Playground
by Taha Shashtari
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>
<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="isOpen = false" @blur="isOpen = false" @keydown.down="moveDown" @keydown.up="moveUp" @keydown.enter="select">
<i class="fa fa-angle-down"></i>
</p>
<ul v-show="isOpen" class="options-list">
<li v-for="(option, index) in fOptions" :class="{
'highlighted': index === highlightedPosition
}" @mouseenter="highlightedPosition = index" @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: ''
}
},
computed: {
fOptions() {
const re = new RegExp(this.keyword, 'i')
return this.options.filter(o => o.title.match(re))
}
},
methods: {
onInput(value) {
this.highlightedPosition = 0
this.isOpen = !!value
},
moveDown() {
if (!this.isOpen) {
return
}
this.highlightedPosition =
(this.highlightedPosition + 1) % this.fOptions.length
},
moveUp() {
if (!this.isOpen) {
return
}
this.highlightedPosition = this.highlightedPosition - 1 < 0 ? this.fOptions.length - 1 : this.highlightedPosition - 1
},
select() {
const selectedOption = this.fOptions[this.highlightedPosition]
this.$emit('select', selectedOption)
this.isOpen = false
this.keyword = selectedOption.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: 'http://lorempicsum.com/nemo/200/200/2'
}, {
title: 'Third Scene',
description: 'lorem ipsum dolor amet.',
thumbnail: 'http://lorempicsum.com/nemo/200/200/3'
}, {
title: 'Fourth Scene',
description: 'lorem ipsum dolor amet.',
thumbnail: 'http://lorempicsum.com/nemo/200/200/4'
}]
},
methods: {
onOptionSelect(option) {
console.log('Selected option:', option)
}
}
})