UNPKG

@tomickigrzegorz/autocomplete

Version:

A lightweight, dependency-free autocomplete component in vanilla JavaScript with WAI-ARIA support.

607 lines (495 loc) 28.2 kB
<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 @tomickigrzegorz/autocomplete ``` Or using Yarn: ```bash yarn add @tomickigrzegorz/autocomplete ``` ### Import via ES module After installing via npm or Yarn you can import the library as an ES module: ```js import Autocomplete from '@tomickigrzegorz/autocomplete'; import '@tomickigrzegorz/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.