Search code examples
javascriptvue.jseventsevent-handlingvue-cli

Vue: Input Manual Autocomplete component


I have a vue-cli project, that has a component named 'AutoCompleteList.vue' that manually handled for searching experience and this component has some buttons that will be fill out the input.

It listens an array as its item list. so when this array has some items, it will be automatically shown; and when I empty this array, it will be automatically hidden.

I defined an oninput event method for my input, that fetches data from server, and fill the array. so the autocomplete list, will not be shown while the user doesn't try to enter something into the input.

I also like to hide the autocomplete list when the user blurs the input (onblur).

but there is a really big problem! when the user chooses one of items (buttons) on the autocomplete list, JS-engine first blurs the input (onblur runs) and then, tries to run onclick method in autocomplete list. but its too late, because the autocomplete list has hidden and there is nothing to do. so the input will not fill out...


here is my code:

src/views/LoginView.vue:

<template>

<InputGroup
    label="Your School Name"
    inputId="schoolName"
    :onInput="schoolNameOnInput"
    autoComplete="off"
    :onFocus="onFocus"
    :onBlur="onBlur"
    :vModel="schoolName"
    @update:vModel="newValue => schoolName = newValue"
/>

<AutoCompleteList
    :items="autoCompleteItems"
    :choose="autoCompleteOnChoose"
    v-show="autoCompleteItems.length > 0"
    :positionY="autoCompletePositionY"
    :positionX="autoCompletePositionX"
/>

</template>

<script>

import InputGroup from '../components/InputGroup'
import AutoCompleteList from '../components/AutoCompleteList'

export default {
    name: 'LoginView',
    components: {
        InputGroup,
        AutoCompleteList
    },
    props: [],
    data: () => ({
        autoCompleteItems: [],
        autoCompletePositionY: 0,
        autoCompletePositionX: 0,
        schoolName: ""
    }),
    methods: {
        async schoolNameOnInput(e) {
            const data = await (await fetch(`http://[::1]:8888/schools/${e.target.value}`)).json();

            this.autoCompleteItems = data;
        },
        autoCompleteOnChoose(value, name) {
            OO("#schoolName").val(name);
            this.schoolName = name;
        },
        onFocus(e) {
            const position = e.target.getBoundingClientRect();
            this.autoCompletePositionX = innerWidth - position.right;
            this.autoCompletePositionY = position.top + e.target.offsetHeight + 20;
        },
        onBlur(e) {
            // this.autoCompleteItems = [];
            // PROBLEM! =================================================================
        }
    }
}

</script>

src/components/AutoCompleteList.vue:

<template>
    <div class="autocomplete-list" :style="'top: ' + this.positionY + 'px; right: ' + this.positionX + 'px;'">
        <ul>
            <li v-for="(item, index) in items" :key="index">
                <button @click="choose(item.value, item.name)" type="button">{{ item.name }}</button>
            </li>
        </ul>
    </div>
</template>

<script>

export default {
    name: 'AutoCompleteList',
    props: {
        items: Array,
        positionX: Number,
        positionY: Number,
        choose: Function
    },
    data: () => ({

    })
}

</script>

src/components/InputGroup.vue:

<template>
    <div class="input-group mb-3">
        <label class="input-group-text" :for="inputId ?? ''">{{ label }}</label>
        <input
            :type="type ?? 'text'"
            :class="['form-control', ltr && 'ltr']"
            :id="inputId ?? ''"
            @input="$event => { $emit('update:vModel', $event.target.value); onInput($event); }"
            :autocomplete="autoComplete ?? 'off'"
            @focus="onFocus"
            @blur="onBlur"
            :value="vModel"
        />
    </div>
</template>

<script>

export default {
    name: 'input-group',
    props: {
        label: String,
        ltr: Boolean,
        type: String,
        inputId: String,
        groupingId: String,
        onInput: Function,
        autoComplete: String,
        onFocus: Function,
        onBlur: Function,
        vModel: String
    },
    emits: [
        'update:vModel'
    ],
    data: () => ({

    }),
    methods: {
        
    }
}

</script>

Notes on LoginView.vue:

  1. autoCompletePositionX and autoCompletePositionY are used to find the best position to show the autocomplete list; will be changed in onFocus method of the input (inputGroup)
  2. OO("#schoolName").val(name) is used to change the value of the input, works like jQuery (but not exactly)
  3. the [::1]:8888 is my server that used to fetch the search results

If there was any unclear code, ask me in the comment

I need to fix this. any idea?


Solution

  • Thank you, @yoduh

    I got the answer.

    I knew there should be some differences between when the user focus out the input normally, and when he tries to click on buttons.

    the key, was the FocusEvent.relatedTarget property. It should be defined in onblur method. here is its full tutorial.

    I defined a property named isFocus and I change it in onBlur method, only when I sure that the focus is not on the dropdown menu, by checking the relatedTarget