npm package: @liedekef/snapselect
Description: SnapSelect is a JavaScript plugin that is designed to improve the functionality of HTML <select> elements. It is lightweight, customizable, and simple to use. SnapSelect offers a modern and sleek interface with advanced features, while remaining highly co
Author: liedekef
Latest version: 1.0.10

# SnapSelect

**SnapSelect** is a lightweight, customizable, and easy-to-use JavaScript plugin designed to enhance the functionality of HTML `<select>` elements. Inspired by popular select plugins like Select2, TomSelect, and Slim Select, SnapSelect offers a modern and sleek interface with advanced features, while remaining highly configurable and performant.

## Examples
### Multiple select
![image](https://github.com/user-attachments/assets/04e243d0-ab37-400e-b1f9-c2aadc0d3863)


### Non multiple select
![image](https://github.com/user-attachments/assets/cc55dcda-606e-4822-8bd8-8a7cceded4b9)


## Features

- **Live Search:** Enable users to quickly filter options as they type.
- **Ajax call:** Remote data with infinite scrolling.
- **Multi-Select Support:** Allows for multiple selections with intuitive tag management.
- **Optgroup Selection:** Easily select or deselect all options within an optgroup.
- **Clear Button:** Option to show a clear button to reset the current selection (works for both single and multi-select).
- **Select All Option:** Add a "Select All" option to quickly select all available options (for multi-select).
- **Customizable Placeholder:** Define custom placeholder text for an empty selection.
- **Custom Search Keywords:** Enhance search functionality with custom keywords for each option.
- **Bootstrap-Inspired Design:** Styled to seamlessly integrate with Bootstrap 5, but easily customizable to match any design system.
- **Callbacks:** `onItemAdd` and `onItemDelete` hooks for reacting to individual selection changes.
- **Accessible:** Built with accessibility in mind, ensuring a better experience for all users.
- **Lightweight:** Minimal footprint, optimized for performance.

## Installation

Include the SnapSelect JavaScript and CSS files in your project:
```html
<link rel="stylesheet" href="/path/to/snapselect/dist/css/snapselect.min.css">
<script src="/path/to/snapselect/dist/js/snapselect.min.js"></script>
```

## Usage

Initialize SnapSelect on your desired `<select>` elements using any selector:

```html
<h3>Select without optgroups</h3>
<!-- Normal Select -->
<label for="selectNormal">Normal Select:</label>
<select id="selectNormal" name="selectNormal" class="snapSelect">
    <option value="AR" data-key="asado Buenos Aires Spanish">Argentina</option>
    <option value="AU" data-key="vegimite Canberra English">Australia</option>
    <option value="BR" data-key="feijoada Brasília Portuguese">Brazil</option>
    <option value="CN" data-key="dumplings Beijing Mandarin">China</option>
    <option value="FR" data-key="croissant Paris French">France</option>
    <option value="IN" data-key="curry New Delhi Hindi">India</option>
    <option value="JP" data-key="sushi Tokyo Japanese">Japan</option>
    <option value="MX" data-key="tacos Mexico City Spanish">Mexico</option>
    <option value="NG" data-key="jollof Abuja English">Nigeria</option>
    <option value="RU" data-key="borscht Moscow Russian">Russia</option>
    <option value="ZA" data-key="braai Pretoria English">South Africa</option>
    <option value="ES" data-key="paella Madrid Spanish">Spain</option>
    <option value="GB" data-key="fish and chips London English">United Kingdom</option>
    <option value="US" data-key="hamburger Washington D.C. English">United States</option>
</select>

<!-- Multiple Select -->
<label for="selectMultiple">Multiple Select:</label>
<select id="selectMultiple" name="selectMultiple" multiple class="snapSelect">
    <option value="AR" data-key="asado Buenos Aires Spanish">Argentina</option>
    <option value="AU" data-key="vegimite Canberra English">Australia</option>
    <option value="BR" data-key="feijoada Brasília Portuguese">Brazil</option>
    <option value="CN" data-key="dumplings Beijing Mandarin">China</option>
    <option value="FR" data-key="croissant Paris French">France</option>
    <option value="IN" data-key="curry New Delhi Hindi">India</option>
    <option value="JP" data-key="sushi Tokyo Japanese">Japan</option>
    <option value="MX" data-key="tacos Mexico City Spanish">Mexico</option>
    <option value="NG" data-key="jollof Abuja English">Nigeria</option>
    <option value="RU" data-key="borscht Moscow Russian">Russia</option>
    <option value="ZA" data-key="braai Pretoria English">South Africa</option>
    <option value="ES" data-key="paella Madrid Spanish">Spain</option>
    <option value="GB" data-key="fish and chips London English">United Kingdom</option>
    <option value="US" data-key="hamburger Washington D.C. English">United States</option>
</select>

<h3>Select with optgroups</h3>
<!-- Select with Optgroups -->
<label for="selectOptgroupNormal">Normal Select with Optgroups:</label>
<select id="selectOptgroupNormal" name="selectOptgroupNormal" class="snapSelect">
    <optgroup label="Africa">
        <option value="ZA" data-key="braai Pretoria English">South Africa</option>
        <option value="NG" data-key="jollof Abuja English">Nigeria</option>
        <option value="EG" data-key="koshari Cairo Arabic">Egypt</option>
    </optgroup>
    <optgroup label="Asia">
        <option value="CN" data-key="dumplings Beijing Mandarin">China</option>
        <option value="IN" data-key="curry New Delhi Hindi">India</option>
        <option value="JP" data-key="sushi Tokyo Japanese">Japan</option>
    </optgroup>
    <optgroup label="Europe">
        <option value="FR" data-key="croissant Paris French">France</option>
        <option value="ES" data-key="paella Madrid Spanish">Spain</option>
        <option value="GB" data-key="fish and chips London English">United Kingdom</option>
    </optgroup>
    <optgroup label="North America">
        <option value="US" data-key="hamburger Washington D.C. English">United States</option>
        <option value="MX" data-key="tacos Mexico City Spanish">Mexico</option>
        <option value="CA" data-key="poutine Ottawa English">Canada</option>
    </optgroup>
    <optgroup label="South America">
        <option value="BR" data-key="feijoada Brasília Portuguese">Brazil</option>
        <option value="AR" data-key="asado Buenos Aires Spanish">Argentina</option>
        <option value="CO" data-key="arepa Bogotá Spanish">Colombia</option>
    </optgroup>
    <optgroup label="Oceania">
        <option value="AU" data-key="vegimite Canberra English">Australia</option>
        <option value="NZ" data-key="hangi Wellington English">New Zealand</option>
        <option value="FJ" data-key="kokoda Suva English">Fiji</option>
    </optgroup>
</select>

<!-- Multiple Select with Optgroups -->
<label for="selectOptgroupMultiple">Multiple Select with Optgroups and data attributes:</label>
<select id="selectOptgroupMultiple" name="selectOptgroupMultiple" multiple class="snapSelect"
    data-live-search="true" 
    data-placeholder="Choose an option" 
    data-max-selections="3"
    data-show-clear-button="true"
    data-select-optgroups="true"
    data-select-all-option="true"
    data-close-on-select="false">
    <optgroup label="Africa">
        <option value="ZA" data-key="braai Pretoria English">South Africa</option>
        <option value="NG" data-key="jollof Abuja English">Nigeria</option>
        <option value="EG" data-key="koshari Cairo Arabic">Egypt</option>
    </optgroup>
    <!-- ... -->
</select>

<script>
    document.addEventListener('DOMContentLoaded', () => {
        SnapSelect('.snapSelect', {
            liveSearch: true,
            placeholder: 'Select an option...',
            showClearButton: true,
            selectOptgroups: true,
            selectAllOption: true,
            closeOnSelect: false,
            maxSelections: 10,
        });
    });
</script>
```

### AJAX — basic remote data

Use the `ajax` option to load options from a remote endpoint. The select element can be left empty; SnapSelect will populate it on open.

```html
<select id="selectAjax" data-placeholder="Search users..."></select>

<script>
    SnapSelect('#selectAjax', {
        liveSearch: true,
        ajax: {
            url: '/api/users',
        }
    });
</script>
```

The server receives `q` (search term), `page`, and `pagesize` as query parameters on every request:

```
GET /api/users?q=john&page=1&pagesize=20
```

Expected response shape:
```json
{
    "data": [
        { "id": 1, "text": "John Doe" },
        { "id": 2, "text": "John Smith" }
    ],
    "hasMore": true
}
```

### AJAX — using processResults to map remote data

Use the `processResults` option to parse the ajax answer and convert it to the expected format. In this example we get TotalRecordCount from the backend, indicating the total number of records

```html
<select id="selectAjax" data-placeholder="Search users..."></select>

<script>
    SnapSelect('#selectAjax', {
        liveSearch: true,
        ajax: {
            url: '/api/users',
            processResults: function(data, search, page) {
                    const records = Array.isArray(data)
                        ? data
                        : (data.Records || []);
                    const total   = data.TotalRecordCount !== undefined
                        ? data.TotalRecordCount
                        : records.length;
                    let hasMore = total > page * pagesize;
                    // the next if covers the case where all rows are returned in 1 go, not respecting paging
                    if (data.TotalRecordCount !== undefined && records.length >= data.TotalRecordCount)
                        hasMore = false;
                    return { results: records, hasMore };
            }
        }
    });
</script>
```

The server receives `q` (search term), `page`, and `pagesize` as query parameters on every request:

```
GET /api/users?q=john&page=1&pagesize=2
```

Example response shape:
```json
{
    "data": [
        { "id": 1, "text": "John Doe" },
        { "id": 2, "text": "John Smith" }
    ],
    "TotalRecordCount": 10
}
```

### AJAX — with extra parameters and POST

Use `data` to pass extra parameters (as a plain object or a function), and `method` to switch to POST. For POST requests, parameters are sent as `multipart/form-data`, which is compatible with traditional server frameworks like WordPress:

```html
<select id="selectAjaxPost" data-placeholder="Search products..."></select>

<script>
    // Plain object — same extra params on every request
    SnapSelect('#selectAjaxPost', {
        ajax: {
            url: '/api/products/search',
            method: 'POST',
            data: {
                category: 'electronics',
                inStock: true
            },
            processResults: function(response) {
                return {
                    results: response.items,
                    hasMore: response.hasNextPage
                };
            }
        }
    });

    // Function — extra params can vary per search/page
    SnapSelect('#selectAjaxPost', {
        ajax: {
            url: '/api/products/search',
            method: 'POST',
            data: function(searchTerm, page) {
                return {
                    category: document.getElementById('categoryFilter').value
                };
            },
            processResults: function(data, search, page) {
                return {
                    results: data.items,
                    hasMore: data.hasNextPage
                };
            }
        }
    });
</script>
```

### AJAX — dynamic URL (country → state cascade)

The `url` option accepts a function, making dependent dropdowns straightforward:

```html
<select id="country" data-placeholder="Select country..."></select>
<select id="state"   data-placeholder="Select state..." disabled></select>

<script>
    SnapSelect('#country', {
        ajax: {
            url: '/api/countries',
            processResults: r => ({ results: r.data, hasMore: r.meta.hasMore })
        }
    });

    document.getElementById('country').addEventListener('change', function() {
        const stateEl = document.getElementById('state');
        stateEl.disabled = !this.value;
        SnapSelect('#state', {
            ajax: {
                url: (search, page) => `/api/states?country=${this.value}&q=${search}&page=${page}`,
                processResults: r => ({ results: r.data, hasMore: r.meta.hasMore })
            }
        });
    });
</script>
```

### AJAX — custom headers and minimum input length

```javascript
SnapSelect('#selectAjax', {
    ajax: {
        url: '/api/search',
        minimumInputLength: 2,   // only fetch after 2 characters are typed
        delay: 400,              // debounce delay in ms
        headers: {
            'Authorization': 'Bearer my-token',
            'X-Custom-Header': 'value'
        },
        processResults: function(data, search, page) {
            return { results: data.results, hasMore: false };
        }
    }
});
```

---

## Events

The change-event and input-event on the underlying `<select>` are still emitted, so you can hook into those as usual.

## Supported Data Attributes

-   `data-live-search="true|false"` (boolean): Enables or disables the search functionality.
-   `data-max-selections="number"` (number): Sets the maximum number of selections allowed.
-   `data-max-items="number"` (number): Alias for `data-max-selections`.
-   `data-placeholder="text"` (string): Sets the placeholder text when no option is selected.
-   `data-show-clear-button="true|false"` (boolean): Shows or hides the clear button (works for both single and multi-select). Deprecated aliases: `data-clear-all-button`, `data-allow-empty`.
-   `data-select-optgroups="true|false"` (boolean): Allows or disallows selecting all options within an optgroup.
-   `data-select-all-option="true|false"` (boolean): Adds or removes the "Select All" option.
-   `data-close-on-select="true|false"` (boolean): Closes or keeps open the dropdown after selection.

## Configuration Options

### Core options

-   `liveSearch` (boolean): Enable live search functionality. Default: `false`.
-   `maxSelections` (number): Maximum number of selections allowed (for multi-select). Alias: `maxItems`. Default: `Infinity`.
-   `placeholder` (string): Custom placeholder text. Default: ``. This causes an empty option to be added at the top if not present.
-   `defaultText` (string): the text shown on the select box if the first option value and string are empty (placeholder takes precedence if present). Default: `Select...'`.
-   `showClearButton` (boolean): Show a button to clear the current selection. For multi-select, clears all tags at once; for single-select, reverts to the placeholder. Default: `true` if `placeholder` is set, `false` otherwise. Deprecated aliases: `clearAllButton`, `allowEmpty`.
-   `selectOptgroups` (boolean): Allow selecting/deselecting all options within an optgroup. Default: `false`.
-   `selectAllOption` (boolean): Add a "Select All" option for multi-select. Default: `false`.
-   `closeOnSelect` (boolean): Close the dropdown after each selection (single-select only). Default: `true`.

### Callbacks

-   `onItemAdd` (function): Called whenever an item is selected. Receives `(value, text)` — the option's `value` attribute and its visible label text. Fired for both single and multi-select. Inside the callback, `this` refers to the underlying `<select>` element.
-   `onItemDelete` (function): Called whenever an item is deselected. Receives `(value, text)`. In multi-select mode this fires per individual item removed (whether via the tag × button or the dropdown checkbox). In single-select mode it also fires for the *previously selected* value when it is replaced by a new selection. Inside the callback, `this` refers to the underlying `<select>` element.

```javascript
SnapSelect('#mySelect', {
    onItemAdd: function(value, text) {
        // `this` is the <select> element
        const form = this.closest('form');
        console.log('Added:', value, text);
    },
    onItemDelete: function(value, text) {
        // `this` is the <select> element
        const form = this.closest('form');
        console.log('Removed:', value, text);
    }
});
```

Both callbacks work in static and AJAX modes and are independent of the native `change` event — each receives exactly the one value that changed, making them more precise than listening to `change` directly on the underlying `<select>`.

### AJAX options

-   `ajax` (object): Enables remote data loading. When set, the dropdown fetches options from a server instead of reading them from the HTML. Default: `null`.
    -   `url` (string|function) **required**: The endpoint URL, or a function `(searchTerm, page) => string` for dynamic URLs. When a function, `this` refers to the underlying `<select>` element. When a function, this also changes the default cache value (see that option).
    -   `method` (string): HTTP method. Default: `'GET'`.
    -   `data` (object|function): Optional. A plain object of extra parameters, or a function `(searchTerm, page) => object`, merged into every request. When a function, `this` refers to the underlying `<select>` element. When a function, this also changes the default cache value (see that option).
    -   `processResults` (function): Optional. Maps the raw server response to `{ results: [{id, text, ...extras}], hasMore: bool }`. `this` refers to the underlying `<select>` element. If omitted, SnapSelect expects the response to be either a plain array or an object with a `results` array and a `hasMore` boolean. Any extra properties on each result item beyond `id` and `text` are stored as `data-*` attributes on the underlying `<option>` element, making them accessible in `onItemAdd`/`onItemDelete` via `this.querySelector(\`option[value="${value}"]\`).dataset`.
    -   `delay` (number): Debounce delay in ms before firing a search request. Default: `300`.
    -   `minimumInputLength` (number): Minimum number of characters required before fetching. Default: `0`.
    -   `cache` (boolean): Cache results per (search + page) key to avoid redundant requests. Default: `false` if `url` or `data` is a function, `true` otherwise.
    -   `headers` (object): Extra HTTP headers to include in every request. Default: `{}`.
    -   `pagesize` (number): Number of items per page sent to the server. Default: `20`.
    -   `loadingText` (string): Text shown in the dropdown while loading. Default: `'Loading...'`.
    -   `noResultsText` (string): Text shown when no results are returned. Default: `'No results found'`.
    -   `errorText` (string): Text shown when the request fails. Default: `'Error loading results'`.

## Public Methods

After initialisation, SnapSelect returns an instance with the following methods:

```javascript
const select = SnapSelect('#mySelect', { /* options */ });

// Clear the current selection
select.clear();

// Force a fresh AJAX fetch (clears cache for the current search term)
select.refresh();

// Clear the entire AJAX response cache
select.clearCache();
```

## Form Validation

SnapSelect respects the native `required` attribute. When a required field is submitted empty, the widget highlights the select with a `snap-select-invalid` class on the `.snap-select-selected` element and renders a validation message below the widget using `.snap-select-validation-message`. Both classes can be styled freely in your CSS.

```html
<select id="selectRequired" name="category" required data-placeholder="Select a category...">
    <option value=""></option>
    <option value="1">Option A</option>
    <option value="2">Option B</option>
</select>
```

The validation message text is sourced from the browser's own `validationMessage`, so it is automatically localised.

## Accessibility (ARIA)

SnapSelect is designed with accessibility in mind and includes ARIA attributes to enhance usability with assistive technologies.

-   The main container is set as `role="combobox"` with `aria-expanded` and `aria-haspopup` attributes.
-   The items container is set as `role="listbox"`.
-   Selected items are dynamically updated with `aria-live="polite"` for screen reader notifications.

## Contributing

Contributions are welcome! Please fork the repository and submit a pull request for any features, bug fixes, or improvements.

## License

SnapSelect is released under the MIT License. See the LICENSE file for more details.
Feel free to use in any condition and modify all as you want.