Calvin Koepke.

The intersection of software and life.

Recently on a project, I had to implement an accessible autocomplete input on a search form. Tim Wright suggested I used Accessible Autocomplete, and I'm glad he did because it's pretty great.

The library targets an element as its render wrapper. The benefit here is that it not only defaults to no dependencies, but also easily incorporates into other frameworks like React. The downside is that you'll have to handle the fallback for non-JavaScript implementation.

The implementation and configuration are very extendible and was fairly straightforward to incorporate, and I would definitely recommend it.

The library hinges on a source parameter to read a list of data and match the current query to the most relevant results.

From the docs:

1
2
3
4
5
6
7
8
9
10
11
12
import autocomplete from 'accessible-autocomplete';

const results = [
    'France',
    'Germany',
    'United Kingdom'
];

autocomplete({
    target: document.querySelector( '#my-input' ),
    source: results
});

Now, when a user types into the input, an accessible list (with optional keyboard navigation) will populate the closest matching results.

Asynchronous Source

In my particular scenario, the results were determined through a third-party source, where the query was sent as a parameter string:

https://external-url/?query=[query]

Thankfully, the library supports a function argument for the source parameter, with two useful arguments:

1
2
3
4
5
6
autocomplete({
    element: document.querySelector( '#my-input-wrapper' ),
    source: ( query, populateResults ) => {
        // Do something.
    }
});

This allows us to do something with the query. But more importantly, it gives us a manual update function that we can push results to.

This lets us do an asynchronous function and push the new source array once we get the response back:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
autocomplete( {
    source: async ( query, populateResults ) => {
        const res = await fetchSource( query );
        populateResults( res );
    }
})

/**
 * @function fetchSource
 * Fetch the source array and return it's JSON response.
 *
 * @param {string} query
 */
async function fetchSource( query ) {
    const res  = await fetch( `https://external-url.com/?query=${encodeURIComponent( query )}` );
    const data = await res.json();
    return data;
}

Of course, this assumes that the response is in the correct format (an array of strings to sift through). If it's not, you can update it before sending it back and updating the results.

Debouncing

One thing to note is that this library doesn't debounce the typing of the keyboard. It does offer a minimum character count before firing off the results. While this is nice, retrieving an external source array seemed as though it required a bit more conservatism:

1
2
3
4
5
6
7
8
import debounce from 'debounce'

autocomplete({
    source: debounce( async ( query, populateResults ) => {
	const res = await fetchSource( query );
	populateResults( res );
    }, 300 )
})

Downsides

The major downsides to this library are that the autocomplete constructor does not return an instance for later reference. Therefore, tracking the autocomplete instance is nearly impossible.

Another major hurdle is that the autocomplete library exposes no internal methods of any kind in order to manipulate the lifecycle, behavior, or state of the input.

These actually ended up being major blockers for use in this project, and I ended up having to hack together a reluctantly "good-enough" solution that will be more difficult to maintain.

Since this library is so close, however, it's likely a great candidate for open source contribution. You can see an open issue for instance creation here.