Skip to content

Instantly share code, notes, and snippets.

@SigurdMW
Last active May 15, 2018 12:06
Show Gist options
  • Save SigurdMW/dfd189ae5b7b05af266e7a5824c78f58 to your computer and use it in GitHub Desktop.
Save SigurdMW/dfd189ae5b7b05af266e7a5824c78f58 to your computer and use it in GitHub Desktop.
<template>
<div class="autocomplete">
<label :for="inputId">{{ label }}</label>
<div class="autocomplete__input-group" :class="{ 'autocomplete__input-group--error': validatonMessage }">
<input
type="text"
:name="inputName"
:id="inputId"
autocomplete="off"
aria-autocomplete="list"
v-model="inputText"
@input="changeText"
class="autocomplete__input"
@keydown.down.prevent="onArrowDown"
@keydown.up.prevent="onArrowUp"
@keydown.enter="onEnter"
@keydown.esc.prevent="onEsc"
@blur="closeOnBlur"
@focus="openOnFocus"
:placeholder="placeholder"
ref="autcompleteInput"
>
<span v-show="isLoading" class="spinner">
<span class="spinner__circle"></span>
</span>
<ul v-show="isOpen && suggestions.length">
<li
v-for="(suggestion, index) in suggestions"
:aria-selected="index === arrowCounter"
:key="index"
:class="{ 'autocomplete__item--selected': index === arrowCounter }"
@click="setResult(suggestion, index)"
>
{{ suggestion.label }}
</li>
</ul>
<span class="sr-only" role="status" aria-live="assertive" aria-relevant="additions">{{ currentFocusedOption }}</span>
</div>
<label :for="inputId" v-show="validatonMessage" class="form-validation form-validation--error">{{ validatonMessage }}</label>
</div>
</template>
<script>
// Thanks to https://alligator.io/vuejs/vue-autocomplete-component/
import { required, minLength } from 'vuelidate/lib/validators';
export default {
name: "CustomAutocompleteComponent",
props: {
label: {
type: String,
required: true
},
placeholder: {
type: String,
required: false,
default: ""
},
validatonMessage: {
type: String,
required: false,
default: ""
},
inputName: {
type: String,
required: false,
default: "email"
},
items: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
required: false,
default: false
},
allowStickySuggestion: {
type: Boolean,
required: false,
default: false
},
stickySuggestion: {
type: Function,
required: false
}
},
data () {
return {
id: this._uid,
inputId: "autocomplete-input-" + this._uid,
inputText: "",
isOpen: false,
arrowCounter: -1,
currentFocusedOption: ""
}
},
mounted() {
document.addEventListener('click', this.handleClickOutside);
},
methods: {
changeText (e) {
this.$emit('autocompleteInput', this.inputText.trim());
this.open();
},
open () {
if (!this.isOpen) this.arrowCounter = -1;
this.isOpen = true;
},
setResult (suggestion, index) {
this.$emit("autocompleteSelected", suggestion);
if (this.allowStickySuggestion) {
if (index !== this.suggestions.length - 1) {
this.inputText = suggestion.label;
} else {
this.inputText = "";
}
} else {
this.inputText = suggestion.label;
}
this.close();
},
onArrowDown (e) {
e.preventDefault();
if (this.arrowCounter < this.suggestions.length - 1) {
this.arrowCounter = this.arrowCounter + 1;
} else {
this.arrowCounter = 0;
}
this.currentFocusedOption = this.suggestions[this.arrowCounter].value;
},
onArrowUp (e) {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter - 1;
} else {
this.arrowCounter = this.suggestions.length - 1;
}
this.currentFocusedOption = this.suggestions[this.arrowCounter].value;
},
onEnter (e) {
if (this.isOpen) {
e.preventDefault();
if (this.arrowCounter > -1) {
this.setResult(this.suggestions[this.arrowCounter], this.arrowCounter);
this.close();
}
}
},
onEsc () {
this.close();
},
closeOnBlur () {
setTimeout(() => {
if (document.activeElement !== this.$refs.autcompleteInput) {
this.close();
}
}, 200);
},
openOnFocus (e) {
if (e.target.value && !this.isOpen) {
this.open();
}
},
close () {
this.currentFocusedOption = "";
this.isOpen = false;
},
handleClickOutside (evt) {
if (!this.$el.contains(evt.target)) {
this.close();
}
}
},
computed: {
suggestions () {
if (this.items.length) {
if (typeof this.items[0] !== "object") throw new Error("Items in autocomplete must be a list of objects with a label and a value.");
}
let items = this.items.filter(item => item.label.toUpperCase().indexOf(this.inputText.toUpperCase().trim()) > -1);
if (this.allowStickySuggestion) {
const stickySuggestion = this.stickySuggestion(this.inputText);
items.push(stickySuggestion);
}
if (items.length) return items;
return [];
}
},
destroyed () {
document.removeEventListener('click', this.handleClickOutside);
}
}
</script>
// USAGE:
// takes a list of objects with a label and a value
<custom-autocomplete-component
label="Search existing users"
:items="autocompleteData"
:isLoading="isAutocompleteFetching"
:allowStickySuggestion="true"
:stickySuggestion="function (text) { return { label: 'Add new user', value: 'ADD_NEW_USER'}}"
@autocompleteSelected="handleAddNewUser"
@autocompleteInput="handleGetSuggestions"
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment