Skip to content

Instantly share code, notes, and snippets.

@4lg4
Last active July 24, 2018 06:00
Show Gist options
  • Save 4lg4/a3d064a06f50a936f4d920c5d4cd43c3 to your computer and use it in GitHub Desktop.
Save 4lg4/a3d064a06f50a936f4d920c5d4cd43c3 to your computer and use it in GitHub Desktop.
Autocomplete input with debounce
<template>
<div class="AppAutocomplete">
<input v-model="theValue" @input="change" :placeholder="placeholder" @focus="focus" @blur="focus('blur')" ref="input" autocomplete="nope" />
<div class="clear" @click="clearAll" v-if="focused">
<img src="/static/img/clear.png" />
</div>
<AppLoaderBar v-if="loading"/>
<div class="suggestions">
<ul>
<li v-for="(item, index) in theList" :key="index" @click="selected(item)">
<div>{{ item.formattedAddress }}</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import AppLoaderBar from '@/components/AppLoaderBar';
import AppButton from '@/components/AppButton';
const debounce = function debounce(func) {
const wait = 300;
let timeout;
return function(...args) {
// eslint-disable-next-line no-invalid-this
const ctx = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(ctx, args), wait);
};
};
export default {
name: 'AppAutocomplete',
components: {AppLoaderBar, AppButton},
props: {
placeholder: {
type: String,
default: '',
},
value: {
type: [String, Object],
default: '',
},
delay: {
type: Number,
default: 0,
},
list: {
type: Array,
default: undefined,
},
},
data() {
return {
theValue: this.value,
theList: this.list,
selectedItem: undefined,
loading: false,
focused: false,
searched: false,
requestInProgress: false,
runRequestAgain: false,
};
},
methods: {
focusClick() {
this.$refs.input.focus();
this.focus();
},
focus(blur) {
this.focused = true;
this.$emit('focus', true);
},
change(evt) {
this.selectedItem = undefined;
if (this.theValue.length === 0) {
return this.clearList();
}
if (this.theValue.length < 4) {
return true;
}
this.$emit('error', ''); // clear the error
this.loading = true;
return this.changeDebounced();
},
/* eslint-disable no-invalid-this */
changeDebounced: debounce(function() {
this.$emit('change');
this.getList();
return true;
}),
/* eslint-enable no-invalid-this */
getList: async function() {
if (this.requestInProgress) {
this.runRequestAgain = true;
return false;
}
this.requestInProgress = true;
if (this.theValue.length === 0) {
this.loading = false;
return this.clearList();
}
try {
this.theList = await this.$http
.get('suggestions', {
params: {
signed_request: this.context.signed_request,
query: this.theValue,
},
})
.then(({data})=> data.suggestions);
} catch (err) {
console.error('ERROR', err);
const response = err.response || {};
let errMsg = `backend`;
if (response.status === 404) {
this.clearList();
}
if (response.status !== 404 && typeof response !== 'string') {
this.$emit('error', errMsg);
}
}
this.loading = false;
this.searched = true;
this.requestInProgress = false;
if (this.runRequestAgain) {
this.runRequestAgain = false;
this.getList();
}
},
selected(item) {
this.selectedItem = item;
this.clearList();
this.theValue = item.formattedAddress;
this.$emit('focus', false);
},
selectedEmit() {
this.$emit('input', this.selectedItem);
},
clearAll() {
this.theValue = '';
this.clearList();
},
clearList() {
this.searched = false;
this.theList = [];
},
},
updated() {
// console.log('Autocomplete updated', this.theList);
},
};
</script>
<style scoped>
.AppAutocomplete {
width: 100%;
/* height: 100px; */
position: relative;
}
.AppAutocomplete input {
-webkit-appearance: none;
border-radius: 0;
width: 100%;
/* font-size: 30px; */
border: 1px solid #e7e7e7;
/* border-radius: 5px; */
height: 60px;
padding: 0 10px;
/* font-weight: 200; */
color: #475560;
}
.AppAutocomplete input::placeholder {
color: rgba(58, 73, 89, 0.74);
text-align: center;
/* font-weight: 100; */
/* font-size: 20px; */
}
.suggestions {
width: 100%;
/* height: 100%; */
overflow: auto;
padding: 0 0 200px 0;
}
ul {
width: 100%;
margin: 0;
padding: 0;
}
li {
width: 100%;
margin: 0;
padding: 15px 10px;
background: #ffffff;
cursor: pointer;
list-style-type: none;
/* border-radius: 5px; */
border: 1px solid #e5e5e5;
border-top: 0;
height: 52px;
}
li div {
width: 100%;
white-space: nowrap;
overflow: hidden;
color: #475560;
font-size: 14px;
}
li:hover {
background: lightgrey;
}
.clear {
position: absolute;
top: 1px;
right: 1px;
height: 58px;
width: 40px;
text-align: center;
padding: 15px 0;
cursor: pointer;
}
</style>
<TEST>
<script>
import Component from '@/components/AppAutocomplete';
import {createLocalVue, mount} from '@vue/test-utils';
import MyStore from '@/core/MyStore';
const localVue = createLocalVue();
localVue.use(MyStore);
const propsData = {
value: 'Alga.me',
delay: 500,
};
describe('AppAutocomplete.vue', () => {
const wrapper = mount(Component, {
localVue,
propsData,
});
const component = wrapper.vm;
const input = ()=> wrapper.find('input');
it('should render correct contents', () => {
expect(component.$el.classList.contains('AppAutocomplete')).toEqual(true);
expect(input()).toBeDefined();
expect(wrapper.find('.suggestions')).toBeDefined();
expect(input().element.value).toEqual(propsData.value);
});
it(`should change data theValue as input content change`, ()=> {
const newValue = 'Alga.me/newValue';
input().element.value = newValue;
input().trigger('input');
expect(component.theValue).toEqual(newValue);
});
it(`should wait ${propsData.delay} miliseconds before execute`, async ()=> {
const newValue = 'Alga.me/newValue/withDelay';
input().element.value = newValue;
input().trigger('input');
const result = await new Promise((resolve, reject)=> {
component.change();
setTimeout(()=> resolve(wrapper.emitted('change')), propsData.delay + 500); // add more 500 milliseconds to be safe to get some result from the input
});
expect(result).toBeDefined();
});
it(`should receive an Array of objects [{id, formattedAddress}] from the enpoint`, async ()=> {
const newValue = 'wollstonecraft';
input().element.value = newValue;
input().trigger('input');
expect(component.theValue).toEqual(newValue);
await component.getList();
expect(component.theList).toBeDefined();
expect(component.theList instanceof Array).toEqual(true);
expect(component.theList[0]).toHaveProperty('id');
expect(component.theList[0]).toHaveProperty('formattedAddress');
});
it(`should render a list of suggestions from the returned list`, ()=>
expect(wrapper.find('.suggestions ul').element.childNodes.length).toBeGreaterThan(0)
);
it(`should fulfill the autocomplete input with the selection and emit an event with the result object`, ()=> {
wrapper.find('.suggestions ul li').trigger('click');
const emitted = wrapper.emitted('input')[0][0];
expect(emitted).toBeDefined();
expect(emitted).toHaveProperty('id');
expect(emitted).toHaveProperty('formattedAddress');
});
});
</script>
</TEST>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment