Vue select-search

Vue select-search is a component that allows you to search select options. Items component receives must be an array of objects, preferably containing id key. Uses Vue, Lodash, Bootstrap 4 styling and few FontAwesome 4 icons

html

<div id='app'>
    <div class='container'>
        <h1>Vue <strong>&lt;select-search&gt;</strong> component</h1>
        <p>Use arrow keys (up and down) on the input field to switch between previous and next item</p>
        <p>Hit enter on the input field to select, or use your mouse to manually select from select box</p>
        <select-search v-model='firstExample' :items='ppl' :shows='"name"'></select-search>
        <p>Result: {{ firstExample }}</p>
        <p>First option doesn't necessarily have to be disabled</p>
        <select-search v-model='secondExample' :items='ppl' :shows='"name"' :first-disabled='false' :first-label='"ALL"'></select-search>
        <p>Result: {{ secondExample }}</p>
        <p>Button at the end to make it feel like an inline form</p>
        <select-search v-model='thirdExample' :items='ppl' :shows='"name"' :add-btn='true' @selected='thirdExampleSelected'></select-search>
        <p>Smashing enter on the input or clicking the button emits an 'selected' event, so you can listen for it on the parent</p>
        <p>Customize which item key is returned as a result (default: id)</p>
        <select-search v-model='fourthExample' :items='ppl' :shows='"name"' :returns='"name"'></select-search>
        <p>Result: {{ fourthExample }}</p>
    </div>
</div>

<template id='my-template'>
    <div class='input-group col' style='padding:0'>
        <div class='input-group-prepend'>
            <span class='input-group-text'><i class='fa fa-search'></i>&nbsp;{{ getCount() }}</span>
        </div>
        <input type='text' class='form-control'
            v-model='searchQuery'
            placeholder='Search'
            @keyup.enter='addItem'
            @keyup.up='prevItem'
            @keyup.down='nextItem'
            @keyup.esc='close'
        >
        <select class='form-control d-inline' v-model='selectedItem' @change='manualChange'>
            <option :disabled='firstDisabled' value=0>
                <template v-if='addedItem == 0 && !firstDisabled'>=></template> {{ firstLabel }}
            </option>
            <option v-for='item in searchedItems' :key='item.id' :value='item[returns]'>
                <template v-if='addedItem == item[returns]'>=></template> {{ formOptionText(item) }}
            </option>
        </select>
        <div class='input-group-append' v-if='addBtn'>
            <button class='btn btn-primary' @click='addItem'><i class='fa fa-chevron-right'></i></button>
        </div>
    </div>
</template>

Css

*
  outline: none
  box-sizing: border-box

.container
    padding-top: 20px

JavaScript

Vue.component('select-search', {
    template: '#my-template',
    props: {
        value: {
            required: true
        },
        items: {
            type: Array,
            required: true
        },
        returns: {
            type: String,
            default: 'id'
        },
        shows: {
            type: String,
            required: true
        },
        firstLabel: {
            type: String,
            default: 'Please select'
        },
        firstDisabled: {
            type: Boolean,
            default: true
        },
        search: {
            type: String,
            default: ''
        },
        addBtn: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            searchQuery: '',
            addedItem: this.value || 0,
            selectedItem: this.value || 0
        }
    },
    computed: {
        columns() {
            if(!this.items.length) return []
            return Object.keys(this.items[0])
        },
        searchColumns() {
            if(!this.search.length) return this.columns
            return this.search.split('|')
        },
        searchedItems() {
            if(!this.searchQuery) {
                this.selectedItem = 0
                if(this.addedItem !== 0 && this.checkItemExists(this.addedItem)) this.selectedItem = this.addedItem
                else this.addedItem = 0
                return this.items
            }

            let items = this.items.filter(item => {
                return this.searchColumns.some((column) => {
                    return String(item[column]).toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1
                })
            })

            if(items.length) this.selectedItem = items[0][this.returns]
            else this.selectedItem = 0

            return items
        }
    },
    methods: {
        checkItemExists(item) {
            return _.map(this.items, this.returns).indexOf(item) > -1
        },
        addItem() {
            //if(this.selectedItem == 0) return
            this.addedItem = this.selectedItem

            if(this.addedItem == 0 && this.firstDisabled) return

            this.$emit('input', this.addedItem)
            this.$emit('selected')
        },
        prevItem() {
            if(!this.items.length) return

            let i = this.searchedItems.map(itm => itm[this.returns]).indexOf(this.selectedItem)

            if(i == 0) {
                if(this.firstDisabled) this.selectedItem = this.searchedItems[this.searchedItems.length - 1][this.returns]
                else this.selectedItem = 0
            } else if(this.selectedItem == 0) this.selectedItem = this.searchedItems[this.searchedItems.length - 1][this.returns]
            else this.selectedItem = this.searchedItems[i - 1][this.returns]
        },
        nextItem() {
            if(!this.items.length) return

            let i = this.searchedItems.map(itm => itm[this.returns]).indexOf(this.selectedItem)

            if(i == this.searchedItems.length - 1) {
                if(this.firstDisabled) this.selectedItem = this.searchedItems[0][this.returns]
                else this.selectedItem = 0
            }
            else this.selectedItem = this.searchedItems[i + 1][this.returns]
        },
        close() {
            this.$emit('close ')
        },
        formOptionText(item) {
            let j = []
            let s = this.shows.split('|')
            s.forEach(x => j.push(item[x]))

            return j.join(' | ')
        },
        getCount() {
            return this.firstDisabled ? this.searchedItems.length : this.searchedItems.length + 1
        },
        manualChange() {
            this.addedItem = this.selectedItem
            this.$emit('input', this.addedItem)
        }
    }
});

const ppl = [
    {
        id: 1,
        name: 'John Doe'
    },
    {
        id: 2,
        name: 'John Cena'
    },
    {
        id: 3,
        name: 'Sylvester Stallone'
    }
]

new Vue({
    el: '#app',
    data: {
        ppl: ppl,
        firstExample: 0,
        secondExample: 0,
        thirdExample: 0,
        fourthExample: 0
    },
    methods: {
        thirdExampleSelected() {
            alert('Result: ' + this.thirdExample)
        }
    }
});

Author

Goran Petričević

Demo

See the Pen Vue select-search by Goran Petričević (@goranp) on CodePen.