@tomickigrzegorz/autocomplete
Version:
A lightweight, dependency-free autocomplete component in vanilla JavaScript with WAI-ARIA support.
607 lines (495 loc) • 28.2 kB
Markdown
<h1 align="center">
Autocomplete
</h1>
<p align="center">
Simple autocomplete with asynchronous data fetch
</p>
<p align="center">
<img src="https://img.shields.io/github/package-json/v/tomickigrzegorz/autocomplete">
<img src="https://img.shields.io/github/size/tomickigrzegorz/autocomplete/dist/js/autocomplete.min.js">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-MIT-green.svg">
</a>
</p>
<p align="center">
<img src="static/examples.png">
</p>
## Demo
See the demo - [example](https://tomickigrzegorz.github.io/autocomplete/)
## Select2 replacement
This library can serve as a lightweight, zero-dependency alternative to **jQuery Select2**. Using `dropdownParent`, `showValuesOnClick` and `classPrefix` you can build fully custom select components — including flag-based country selectors and multi-select with chip tags — without pulling in jQuery or any extra dependencies.
See the live examples:
- [Country selector](https://tomickigrzegorz.github.io/autocomplete/#country-select-example) — single-select with flag icons
- [Country selector multi](https://tomickigrzegorz.github.io/autocomplete/#country-select-multi-example) — multi-select with chips, configurable limit and deselect on click
## Features
- You're in full control of the DOM elements to output
- Accessible, with full support for ARIA attributes and keyboard interactions
- Customize your own CSS
- Support for asynchronous data fetching
- Move between the records using the arrows <kbd>↓</kbd> <kbd>↑</kbd>, and confirm by <kbd>Enter</kbd> or mouse
- Grouping of record results
- Showing 'no results'
- Show all values on click
- Multiple choices
- **Select2 alternative** — build custom searchable selects with flags, chips and multi-select without jQuery
- No dependencies
- Very light library, packed gzip **only ~4KB**
- And a lot more
## Installation
### Using npm
Install the library via npm:
```bash
npm install /autocomplete
```
Or using Yarn:
```bash
yarn add /autocomplete
```
### Import via ES module
After installing via npm or Yarn you can import the library as an ES module:
```js
import Autocomplete from '/autocomplete';
import '/autocomplete/css';
```
Or import CSS separately using the full path:
```js
import 'node_modules/@tomickigrzegorz/autocomplete/dist/css/autocomplete.min.css';
```
### Using a CDN
#### CSS
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tomickigrzegorz/autocomplete@3.3.2/dist/css/autocomplete.min.css"/>
```
#### JavaScript
```html
<script src="https://cdn.jsdelivr.net/gh/tomickigrzegorz/autocomplete@3.3.2/dist/js/autocomplete.min.js"></script>
```
##### -- OR --
Download from `dist` folder and insert to HTML:
- `dist/css/autocomplete.css`
- `dist/js/autocomplete.min.js`
#### HTML
Basic code to display autocomplete correctly
```html
<div class="auto-search-wrapper">
<input type="text" id="local" autocomplete="off" placeholder="Enter letter" />
</div>
```
#### JavaScript
```js
window.addEventListener('DOMContentLoaded', function () {
// 'local' is the 'id' of input element
new Autocomplete('local', {
onSearch: ({ currentValue }) => {
// local data
const data = [
{ name: 'Walter White' },
{ name: 'Jesse Pinkman' },
{ name: 'Skyler White' },
{ name: 'Walter White Jr.' },
];
return data
.sort((a, b) => a.name.localeCompare(b.name))
.filter((element) => {
return element.name.match(new RegExp(currentValue, 'i'));
});
},
onResults: ({ matches }) => {
return matches
.map((el) => {
return `
<li>${el.name}</li>`;
})
.join('');
},
});
});
```
## Package Manager
Before the first use, clone this repository and install node dependencies:
```js
git clone https://github.com/tomickigrzegorz/autocomplete.git
yarn
// or
npm install
```
## Run the app
Run the development version:
```js
yarn dev
// or
npm run dev
```
Run the production version:
```js
yarn prod
// or
npm run prod
```
## Configuration of the plugin
| props | type | default | require | description |
| -------------------- | :--------: | :---------------------------------: | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| element | string | | ✔ | Input field id |
| onSearch | function | | ✔ | Function for user input. It can be a synchronous function or a promise |
| onResults | function | | ✔ | Function that creates the appearance of the result |
| onSubmit | function | | | Executed on input submission |
| onOpened | function | | | returns two variables 'results' and 'showItems', 'resutls' first rendering of the results 'showItems' only showing the results when clicking on the input field |
| onSelectedItem | function | | | Get index and data from li element after hovering over li with the mouse or using arrow keys ↓/↑ |
| onReset | function | | | After clicking the 'x' button |
| onRender | function | | | Possibility to add html elements, e.g. before and after the search results |
| onClose | function | | | e.g. delete class after close results, see example modal |
| noResults | function | | | Called when no results are found. Return an HTML string to display: `noResults: ({ currentValue }) => \`<li>No results for "${currentValue}"</li>\`` |
| onLoading | function | | | Called when an async search starts. Return an HTML string to display in the dropdown while waiting for results (e.g. a spinner or status message). Replaced automatically when results or `noResults` fires: `onLoading: ({ currentValue }) => \`<li>Searching for "${currentValue}"…</li>\`` |
| destroy | method | | | Clears the input and removes all event listeners. Use `reset()` if you want to keep the autocomplete functional |
| reset | method | | | Clears the input and closes the results list while keeping all event listeners active. Safe alternative to `destroy()` when you just want to clear the field |
| rerender | method | | | This method allows you to re-render the results without modifying the input field. Of course, we can also send the string we want to search for to the method. render(string); |
| disable | method | | | This method allows you to disable the autocomplete functionality. `const auto = new Autocomplete('id', {...});` `auto.disable();` then we disable the autocomplete. To remove input value you need to call `auto.disable(true);` |
| enable | method | | | This method allows you to re-enable the autocomplete functionality after it has been disabled. `const auto = new Autocomplete('id', {...});` `auto.disable();` `auto.enable();` - now the autocomplete is active again and all event listeners are restored |
| clearButton | boolean | `true` | | A parameter set to 'true' adds a button to remove text from the input field |GitHub Markdown Preview
| clearButtonOnInitial | boolean | `false` | | A parameter set to 'true' adds a button to remove text from the input field visible on initial Autocomplete lib. |
| selectFirst | boolean | `false` | | Default selects the first item in the list of results |
| insertToInput | boolean | `false` | | Adding an element selected with arrows or hovering with the mouse to the input field |
| disableCloseOnSelect | boolean | `false` | | Prevents results from hiding after clicking on an item from the results list |
| preventScrollUp | boolean | `false` | | Prevents the results from scrolling to the top when the dropdown reopens. The scroll position and the highlighted selection are both preserved — clicking the input or closing and reopening the dropdown keeps the previously selected item highlighted. |
| showAllValuesOnClick | boolean | `false` | | This option will toggle showing all values when the input is clicked, like a default dropdown |
| inline | boolean | `false` | | This option displays all results without clicking on the input field |
| removeResultsWhenInputIsEmpty | boolean | `false` | | Set to `true` to clear the results when the input is empty |
| cache | boolean | `false` | | The characters entered in the input field are cached |
| regex | object | `{ expression: /[\|\\{}()[\]^$+*?]/g, replacement: "\\$&" }` | | the parameter allows you modify string before search. For example, we can remove special characters from the string. Default value is object `{ expression: /[\|\\{}()[\]^$+*?]/g, replacement: "\\$&" }` You can add only `expression` or only `replacement`. |
| howManyCharacters | number | `1` | | The number of characters entered should start searching |
| delay | number | `500` | | Time in milliseconds that the component should wait after last keystroke before calling search function 1000 = 1s |
| ariaLabelClear | string | `clear the search query.` | | Set aria-label attribute for the clear button |
| classPreventClosing | string | | | Prevents results from hiding after clicking on element with this class |
| classGroup | string | | | Enter a class name, this class will be added to the group name elements |
| classPrefix | string | | | Prefixing all autocomplete css class name, 'prefix-auto-', default 'auto-' |
| dropdownParent | string/Element | `null` | | Appends the dropdown to the specified element instead of next to the input. Accepts a CSS selector string or an `HTMLElement`. Use `document.body` to escape `overflow` clipping in modals or fixed-height containers. |
| dropdownAttrs | object | `{}` | | Extra HTML attributes applied to the dropdown wrapper when `dropdownParent` is set. Supports `class` (adds CSS classes) and `style` (inline CSS string, e.g. `"z-index: 10001"` to override the default). To style the inner result list use a descendant selector: `.my-wrapper ul { max-height: 200px; overflow-y: auto; }` |
**instructions** - has been removed from the library, [see how to add to html](https://tomickigrzegorz.github.io/autocomplete/#complex-example)
## How do I add data to the input field?
### Simple data
```js
onResults: ({ matches }) => {
return matches
.map((el) => {
return `
<li>${el.name}</li>`;
})
.join('');
};
```
### A complicated example
The example below displays `${el.name}`, first name and last name as well as `${el.img}` photo in the results. From this example, only the first element will be added to the input field. So `${el.name}` no matter if it will be inside `p`, `div`, `span` etc. Always the first element and it's only text so it can even be in this form `<p><b>${el.name}</b></p>`
```js
onResults: ({ matches }) => {
return matches
.map((el) => {
return `
<li>
<p>${el.name}</p>
<p><img src="${el.img}"></p>
</li>`;
})
.join('');
};
```
## Usage jquery || axios || promise + fetch
JAVASCRIPT
```js
new Autocomplete('complex', {
// search delay
delay: 1000,
// add button 'x' to clear the text from
// the input filed
// by default is true
clearButton: true,
// show button 'x' to clear the text from
// the input filed on initial library
clearButtonOnInitial: false,
// default selects the first item in
// the list of results
// by default is false
selectFirst: false,
// add text to the input field as you move through
// the results with the up/down cursors
// by default is false
insertToInput: false,
// the number of characters entered
// should start searching
// by default is 1
howManyCharacters: 1,
// the characters entered in
// the input field are cached
// by default is false
cache: false,
// prevents results from hiding after
// clicking on an item from the list
// by default is false
disableCloseOnSelect: false,
// enter the name of the class by
// which you will name the group element
// by default is empty ''
classGroup: 'group-by',
// prevents results from hiding after
// clicking on element with this class
// of course, any class name
// by default is empty ''
classPreventClosing: 'additional-elements',
// prefixing all autocomplete css class name,
// 'prefix-auto-', default 'auto-'
classPrefix: 'prefix',
// this option will toggle showing all
// values when the input is clicked,
// like a default dropdown
// by default is false
showAllValuesOnClick: false,
// this option displays all results
// without clicking on the input field
inline: false,
// set aria-label attribute for the clear button
// by default is 'clear text from input'
ariaLabelClear: 'insert your text if you want ;)'
// parameter prevents the results from scrolling
// up when we have scrolling. It also works when we
// click a second time when we have results.
// The results are shown in the same place.
preventScrollUp: false,
// set to true deletes the results when input is empty.
// We use the `reset()` method which removes the
// results from the DOM and returns everything to its
// original state
removeResultsWhenInputIsEmpty: false,
// appends the dropdown to the specified element instead
// of next to the input. Accepts a CSS selector string
// or an HTMLElement — useful to escape overflow clipping
// in modals or fixed-height containers.
// by default is null
dropdownParent: document.body,
// extra HTML attributes applied to the dropdown wrapper
// element when dropdownParent is set.
// `class` adds CSS classes, `style` sets inline CSS
// (e.g. to override the default z-index: 9999).
// To style the inner result list (e.g. height / scroll),
// use a CSS descendant selector: .my-wrapper ul { max-height: 200px; overflow-y: auto; }
dropdownAttrs: { class: "my-wrapper", style: "z-index: 10001" },
// parameter allows you modify string before search.
// For example, we can remove special characters from
// the string. Default value is object
// `{ expression: /[|\\{}()[\]^$+*?]/g, replacement: "\\$&" }`
// You can add only `expression` or only `replacement`.
regex: { expression: /[\|\\{}()[\]^$+*?]/g, replacement: "\\$&" },
// Function for user input. It can be a synchronous function or a promise
// you can fetch data with jquery, axios, fetch, etc.
onSearch: async ({ currentValue }) => {
// static file
// const api = './characters.json';
// OR -------------------------------
// your REST API
const api = `https://rickandmortyapi.com/api/character?name=${encodeURI(currentValue)}`;
/**
* jquery
* If you want to use jquery you have to add the
* jquery library to head html
* https://cdnjs.com/libraries/jquery
*/
return $.ajax({
url: api,
method: 'GET',
})
.done(function (data) {
return data.results;
})
.fail(function (xhr) {
console.error(xhr);
});
// OR ----------------------------------
/**
* axios
* If you want to use axios you have to add the
* axios library to head html
* https://cdnjs.com/libraries/axios
*/
return axios
.get(api)
.then((response) => {
return response.data.results;
})
.catch((error) => {
console.log(error);
});
// OR ----------------------------------
const response = await fetch(api);
const data = await response.json();
return data.results;
},
// this part is responsible for the number of records,
// the appearance of li elements and it really depends
// on you how it will look
onResults: ({ currentValue, matches, classGroup }) => {
// const regex = new RegExp(^${input}`, 'gi'); // start with
const regex = new RegExp(currentValue, 'gi');
// counting status elements
function count(status) {
let count = {};
matches.map((el) => {
count[el.status] = (count[el.status] || 0) + 1;
});
return `<small>${count[status]} items</small>`;
}
return matches
.sort(
(a, b) =>
a.status.localeCompare(b.status) || a.name.localeCompare(b.name)
)
.map((el, index, array) => {
// we create an element of the group
let group =
el.status !== array[index - 1]?.status
? `<li class="${classGroup}">${el.status} ${count(el.status)}</li>`
: '';
// this part is responsible for the appearance
// in the drop-down list - see the example in index.html
// remember only the first element from <li> is put
// into the input field, in this case the text
// from the <p> element
return `
${group}
<li>
<h2 style="margin-bottom: 10px;">
${el.name.replace(regex, (str) => `<b style="color: red;">${str}</b>`)}
</h2>
<div style="display: flex;">
<div style="margin-right: 10px;">
<img src="${el.image}" style="max-width: 67px;max-height:95px">
</div>
<div class="info">
<h4>${el.name}</h4>
<div><b>gender:</b> - ${el.gender}</div>
<div><b>species:</b> - ${el.species}</div>
<div><b>status:</b> - ${el.status}</div>
</div>
</div>
</li>`;
})
.join('');
},
// the onSubmit function is executed when the user
// submits their result by either selecting a result
// from the list, or pressing enter or mouse button
onSubmit: ({ index, element, object, results }) => {
console.log('complex: ', index, element, object, results);
// window.open(`https://www.imdb.com/find?q=${encodeURI(input)}`)
},
// get index and data from li element after
// hovering over li with the mouse or using
// arrow keys ↓ | ↑
onSelectedItem: ({ index, element, object, currentValue }) => {
console.log('onSelectedItem:', index, element.value, object);
},
// the callback presents no results
noResults: ({ currentValue }) =>
`<li>No results found: "${currentValue}"</li>`,
// called when async search starts — return HTML to show in the
// dropdown while waiting for results (replaced automatically
// when results or noResults fires)
onLoading: ({ currentValue }) =>
`<li>Searching for "${currentValue}"…</li>`,
});
```
## All available configuration items
```js
const auto = new Autocomplete('you-id', {
clearButton: true,
clearButtonOnInitial: false,
selectFirst: false,
insertToInput: false,
disableCloseOnSelect: false,
cache: false,
showAllValuesOnClick: false,
inline: false,
howManyCharacters: 1,
preventScrollUp: false,
delay: 500,
ariaLabelClear: "clear the search query",
regex: { expression: /[\|\\{}()[\]^$+*?]/g, replacement: "\\$&" },
removeResultsWhenInputIsEmpty: false,
dropdownParent: null, // string (CSS selector) or HTMLElement
dropdownAttrs: {}, // extra attrs on wrapper: { class, style } — only with dropdownParent
classPreventClosing: "", // don't use empty value
classGroup: "", // don't use empty value
classPrefix: "", // don't use empty value
onSearch: ({ currentValue, element }) => {},
onResults: ({ currentValue, matches, classGroup }) => {},
onRender: ({ element, results }) => {},
onSubmit: ({ index, element, object, results }) => {},
onOpened: ({ type, element, results }) => {},
onSelectedItem: ({ index, element, object, currentValue }) => {},
onReset: (element) => {},
onClose: () => {},
noResults: ({ element, currentValue }) => {},
onLoading: ({ element, currentValue }) => {},
});
// public methods
auto.destroy(); // clear input and remove all event listeners
auto.reset(); // clear input and close results, keeps listeners active
auto.disable(); // disable autocomplete
auto.disable(true); // disable autocomplete and clear input value
auto.enable(); // enable autocomplete after it was disabled
auto.rerender(); // re-render the results
// pass string to search
auto.rerender(string);
```
### Enable/Disable Example
The `enable()` method safely restores autocomplete functionality without automatically triggering search results:
```js
const auto = new Autocomplete('input', {
onSearch: ({ currentValue }) => {
// your search logic
return data.filter(item => item.name.includes(currentValue));
}
});
// Disable autocomplete
auto.disable();
// Re-enable autocomplete - this will NOT trigger search automatically
// even if the input field is empty or has some text
auto.enable();
// After enable(), autocomplete will work normally:
// - User needs to type (respecting howManyCharacters setting)
// - OR click input (if showAllValuesOnClick: true)
// - OR call auto.rerender() to trigger search programmatically
```
**Important**: `enable()` only restores event listeners and functionality. It doesn't automatically show results. Search is triggered only by:
- User input (when `howManyCharacters` threshold is met)
- Input click (when `showAllValuesOnClick: true`)
- Manual `rerender()` call
### preventScrollUp: selection behavior
When `preventScrollUp: false` (default), clicking the input while the dropdown is open resets the highlighted selection:
- `selectFirst: false` — no item highlighted; ↓ moves to first item, ↑ moves to last
- `selectFirst: true` — first item is highlighted
When `preventScrollUp: true`, clicking the input (or closing and reopening the dropdown) **preserves** the previously highlighted item — both the scroll position and the selection stay exactly where they were.
```js
new Autocomplete('search', {
preventScrollUp: true,
onSearch: async ({ currentValue }) => { ... },
onResults: ({ matches }) => matches.map(...).join(''),
});
// After selecting item #5 and clicking outside to close,
// clicking the input again reopens with item #5 still highlighted.
```
### reset() vs destroy()
| Method | Clears input | Closes dropdown | Removes listeners | When to use |
|--------|-------------|-----------------|-------------------|-------------|
| `reset()` | ✅ | ✅ | ❌ | Clear button, external "clear" action — autocomplete stays functional |
| `destroy()` | ✅ | ✅ | ✅ | Permanent teardown — e.g. closing a modal with `dropdownParent` |
```js
// External clear button — use reset()
document.querySelector('.clear-btn').addEventListener('click', () => {
auto.reset();
});
// Modal close — use destroy() to clean up resultWrap from document.body
modalCloseBtn.addEventListener('click', () => {
modal.style.display = 'none';
auto.destroy();
});
```
## License
This project is available under the [MIT](https://opensource.org/licenses/mit-license.php) license.