JavaScript: autocomplete with validation

JavaScript: autocomplete with validation

In this article we will see how to implement the autocomplete function with validation of the data entered in JavaScript.

In this article we will see how to implement the autocomplete function with validation of the data entered in JavaScript.

The HTML structure

Let's start with the following HTML structure.

<form action="" method="get" class="autosuggest-form">
        <p>Pick a value between those suggested.</p>
        <div>
            <input type="text" class="autosuggest-input" placeholder="Search...">
            <button type="button" class="autosuggest-btn">Search</button>
        </div>
        <div class="suggestions">
            <ul></ul>
        </div>
    </form>

CSS styles

Let's start by defining global CSS styles.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    font-size: 16px;
}

body  {
    margin: 2.5rem auto;
    max-width: 40%;
    padding: 0 1rem;
    font: 1rem/1.5 sans-serif;
    background-color: #fff;
    color: #000;
}

::placeholder {
    color: #555;
}

:focus,
:active {
    outline-style: none;
    box-shadow: none;
}

button {
    appearance: none;
    cursor: pointer;
    border: none;
    background-color: #000;
    color: #fff;
    padding: 0.85rem 1.25rem;
    letter-spacing: .1em;
    text-transform: uppercase;
}

input {
    padding: 0.85rem;
    background-color: #fff;
    border: 1px solid #000;
}

Now let's define the CSS classes that will style the main text field, including the error highlighting and its textual description.

input.autosuggest-input {
    min-width: 250px;
}

.error {
    color: #d00;
    margin: 1rem 0;
}

input.input-error {
    border: 1px solid #d00;
    background-color: #ffebee;
}

The contrast of the red color against the white background is still acceptable from a readability point of view, as is the pinkish background of the input field when an error is reported. It's always a good practice to check readability for validation messages as well.

The suggestion container will be positioned absolutely relative to the main form with these styles.

form {
    position: relative;
}

.suggestions {
    position: absolute;
    top: auto;
    left: 0;
    background-color: #fff;
    border-width: 0 1px 1px 1px;
    border-color: #000;
    border-style: solid;
    min-width: 250px;
    display: none;
}

.suggestions ul {
    list-style: none;
}
.suggestions ul li {
    display: block;
    padding: 0.5rem;
    cursor: pointer;
}
.suggestions ul li:hover {
    background-color: #f5f5f5;
}

The top property set to the value auto will always place the element after the preceding element in the HTML structure, but only if the latter element is not positioned. In our case the suggestion box will be positioned after the div element that contains the input field.

The JavaScript code

We will define various functions which, performed in the established order, will allow us to find the suggestions via AJAX, filter them based on the characters entered by the user and finally validate them, i.e. by informing the user that the inserted text is not present in the showed suggestions.

Let's start with data retrieval via AJAX.

const getData = async () => {
        try {
            const data = await fetch('products.json');
            const res = await data.json();
            return res.products.map(product => { return { title: product.title }; });
        } catch(err) {
            return [];
        }
    };

Through the async/await model and the Fetch API, we retrieve the requested data array and transform it into a new array of objects each having only the title property. In fact, it is on this property that we are going to implement the filter and the validation on the results.

Let's now create three utility functions to allow us to manage the visibility and reset the contents of the suggestion block.

const showSuggeestions = () => {
        document.querySelector('.suggestions').style.display = 'block';
    };

    const hideSuggestions = () => {
        document.querySelector('.suggestions').style.display = 'none';
    };

    const clearSuggestions = () => {
        document.querySelector('.suggestions ul').innerHTML = '';
    };

We then define a function that, given an input text string, creates a new DOM element li to add to the suggestion list with the specified text as a parameter.

const createSuggestion = text => {
        const target = document.querySelector('.suggestions ul');
        const suggestion = document.createElement('li');
        suggestion.className = 'suggestion';
        suggestion.innerText = text;
        target.appendChild(suggestion);
    };

To speed up the access to the suggestions and to manage the reload of the page, we save the data in the web storage using a specific function.

const setData = async () => {
        let data = [];
        try {
            data = await getData();
        } catch(err) {

        }
        sessionStorage.setItem('suggestions', JSON.stringify(data));
    };

The suggestions array, once found, is saved as a JSON string in the web storage. At this point we can filter this array with the term typed by the user:

const getSuggestions = term => {
        const data = sessionStorage.getItem('suggestions');
        if(data === null) {
            return [];
        }
        const suggestions = JSON.parse(data);
        return suggestions.filter(sugg => {
            return sugg.title.toLowerCase().includes(term.toLowerCase());
        });
    };

The array obtained from the web storage is filtered by comparing the lowercase form of the term entered by the user with the lowercase form of the title property. This conversion is necessary because the includes() method of strings is case sensitive.

The main action takes place on the input field, so we need to intercept the text input on this element.

const handleInput = () => {
        const input = document.querySelector('.autosuggest-input');
        input.addEventListener('keyup', function() {
            const value = this.value;
            if(value && value.length >= 3 && !/^\s+$/.test(value)) {
                hideSuggestions();
                clearSuggestions();
                const suggestions = getSuggestions(value);
                if(suggestions.length > 0) {
                    for(const sugg of suggestions) {
                        createSuggestion(sugg.title);
                    }
                    showSuggeestions();
                }
            }  else {
                hideSuggestions();
                clearSuggestions();
            }   
        }, false);
    };

If the user entered at least three valid characters (not spaces), we reset the suggestion container and filter the suggestion array. If there are suggestions, we create and insert the corresponding li elements and display the suggestion container.

After populating the list of suggestions, we must now ensure that by clicking on an item, its text is entered as the value of the input field.

const handleSuggestion = () => {
        const suggestionsContainer = document.querySelector('.suggestions');
        suggestionsContainer.addEventListener('click', e => {
            const el = e.target;
            if(el.matches('.suggestion')) {
                document.querySelector('.autosuggest-input').value = el.innerText;
            }
        }, false);
    };

Since the items in the list are created dynamically, we have to use event delegation using the target property of the event object which gives us a reference to the current li element, from which we can extract the text and insert it as the value of the input field.

Now we can implement data validation by attaching it to the click event on the form button.

const validateSuggestions = () => {
        const data = sessionStorage.getItem('suggestions');
        if(data === null) {
            return [];
        }
        const err = document.querySelector('.error');
        if(err !== null) {
            err.remove();
        }
        const suggestions = JSON.parse(data);
        const input = document.querySelector('.autosuggest-input');
        input.classList.remove('input-error');
        hideSuggestions();
        clearSuggestions();
        const term = input.value;
        const found = suggestions.find(sgg => sgg.title === term);
        if(!found) {
            const error = document.createElement('div');
            error.className = 'error';
            error.innerText = 'You must choose one of the suggested values.';
            document.querySelector('.autosuggest-form').appendChild(error);
            input.classList.add('input-error');
            return false;
        }    
        return true;
    };

    const handleSubmit = () => {
        const btn = document.querySelector('.autosuggest-btn');
        btn.addEventListener('click', () => {
            validateSuggestions();
        }, false);
    };

Validation is done by first resetting the error message and additional styles of the error field and then comparing the value entered in the field against the suggestions array. If the find() method doesn't find a match (ie, it returns undefined), we enter the error message and add the error styles to the input field.

Finally, we run our code when the DOM has been fully loaded.

document.addEventListener('DOMContentLoaded', () => {
        (async() => {
            handleSuggestion();
            await setData();
            handleInput();
            handleSubmit();
        })();
    }, false);

Demo

JavaScript: autocomplete with validation