# ComboBox

Dynamic suggestions as users type in an input field.

[![npm](https://img.shields.io/badge/npm-v4.2.0-blue)](https://www.npmjs.com/package/@preline/combobox) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://preline.co/plugins/combobox.html)

## Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Configuration Options](#configuration-options)
- [JavaScript API](#javascript-api)
- [Events](#events)
- [Common Patterns](#common-patterns)
- [License](#license)

## Overview

The ComboBox component provides an autocomplete input field with dynamic suggestions. It supports both client-side filtering and API-based data fetching, customizable templates, and flexible data rendering options.

**Key Features:**
- Dynamic autocomplete suggestions
- Client-side and API-based data fetching
- Customizable item templates
- Keyboard navigation support
- Programmatic control via JavaScript API
- Event system for selection tracking
- Accessibility attributes (ARIA) built-in

## Installation

To get started, install ComboBox plugin via npm, else you can skip this step if you are already using Preline UI as a package.

```bash
npm i @preline/combobox
```

### CSS

Use [`@source`](https://tailwindcss.com/docs/functions-and-directives#source-directive) to register the plugin's JavaScript path for Tailwind CSS scanning, then [`@import`](https://tailwindcss.com/docs/functions-and-directives#import-directive) the plugin's CSS files into your Tailwind CSS file.

```css
@import "tailwindcss";

/* @preline/combobox */
@source "../node_modules/@preline/combobox/*.js";
@import "./node_modules/@preline/combobox/variants.css";
@import "./node_modules/@preline/combobox/theme.css";
```

### JavaScript

Include the JavaScript that powers the interactive elements near the end of your `</body>` tag:

```html
<script src="./node_modules/@preline/combobox/index.js"></script>
```

### Manual Initialization

Use the `non-auto` entry if you need manual initialization. In this mode, automatic initialization on page load is not included, so the component should be initialized explicitly.

```html
<script type="module">
  import HSCombobox from "@preline/combobox/non-auto.mjs";

  new HSCombobox(document.querySelector("#combobox"));
</script>
```

### Via Bundler

When using a bundler (Vite, webpack, etc.), import the plugin directly as an ES module.

`@preline/combobox` is the auto-init entry: it scans the DOM and initializes matching elements automatically.

```js
import "@preline/combobox";
```

`@preline/combobox/non-auto` is the manual entry: use it when you want explicit control over when initialization happens, either via `autoInit()` or by creating a specific instance yourself.

```js
import HSCombobox from "@preline/combobox/non-auto";

HSCombobox.autoInit();

// Or initialize a specific element manually
const el = document.querySelector("#combobox");
if (el) new HSCombobox(el);
```

### TypeScript

This package ships with TypeScript type definitions. No additional `@types/` package is needed.

## Basic usage

The following example demonstrates the minimal HTML structure required for a combobox component. This is a base template without custom styling - you can apply your own CSS classes and styles as needed. The combobox shows suggestions as the user types.

```html
<div class="inline-flex relative" data-hs-combo-box>
  <div class="relative">
    <input type="text" role="combobox" aria-expanded="false" value="" data-hs-combo-box-input />
  </div>
  
  <div class="absolute z-50" style="display: none;" data-hs-combo-box-output>
    <div class="cursor-pointer" tabindex="0" data-hs-combo-box-output-item>
      <span data-hs-combo-box-search-text="1" data-hs-combo-box-value>1</span>
    </div>
    <div class="cursor-pointer" tabindex="1" data-hs-combo-box-output-item>
      <span data-hs-combo-box-search-text="2" data-hs-combo-box-value>2</span>
    </div>
  </div>
</div>
```

**Structure Requirements:**
- `data-hs-combo-box`: Required on the container element
- `data-hs-combo-box-input`: Required on the input element
- `data-hs-combo-box-output`: Required on the dropdown container
- `data-hs-combo-box-output-item`: Required on each suggestion item
- `data-hs-combo-box-search-text`: Required on the searchable text element within each item
- Proper ARIA attributes (`role="combobox"`, `aria-expanded`)

**Initial State:**
- Set `aria-expanded="false"` on the input initially
- Add `style="display: none;"` to the output container initially
- Output visibility is controlled by the plugin

## Configuration Options

### Data Options

Data options are specified in the `data-hs-combo-box` attribute and various data attributes.

| Attribute | Target Element | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `data-hs-combo-box` | Container | - | - | Activate a ComboBox by specifying on an element. Should be added to the container. |
| `:gap` | Inside `data-hs-combo-box` | number | `5` | Determines the indent from the input field to the dropdown list in pixels. |
| `:viewport` | Inside `data-hs-combo-box` | string \| HTMLElement | `null` | Determines which element will be the parent when calculating the position of the dropdown list. By default it is calculated relative to the document. |
| `:minSearchLength` | Inside `data-hs-combo-box` | number | `0` | Specifies the minimum number of characters that must be entered before the search function becomes active. |
| `:apiUrl` | Inside `data-hs-combo-box` | string \| null | `null` | Specifies the API address for fetching suggestions. |
| `:apiDataPart` | Inside `data-hs-combo-box` | string \| null | `null` | Determines which part of the returned data will be taken as the main data array. If this parameter is empty, then the root is taken as a basis. |
| `:apiQuery` | Inside `data-hs-combo-box` | string \| null | `null` | Defines request parameters, such as limit. |
| `:apiSearchQuery` | Inside `data-hs-combo-box` | string \| null | `null` | Defines the name of the search parameter. |
| `:apiHeaders` | Inside `data-hs-combo-box` | string \| null | `null` | Defines request headers. |
| `:apiSearchPath` | Inside `data-hs-combo-box` | string \| null | `null` | A string that specifies the endpoint path for the API request, excluding the base URL. Note: `apiSearchPath` and `apiSearchDefaultPath` are mutually exclusive with `apiSearchQuery`. |
| `:apiSearchDefaultPath` | Inside `data-hs-combo-box` | string \| null | `null` | A string that defines the default API endpoint path to be used when the input field is empty. Note: `apiSearchPath` and `apiSearchDefaultPath` are mutually exclusive with `apiSearchQuery`. |
| `:outputItemTemplate` | Inside `data-hs-combo-box` | string \| null | Default template | Defines a template for a single item in a dropdown list. |
| `:outputEmptyTemplate` | Inside `data-hs-combo-box` | string \| null | Default template | Defines a template for an empty placeholder. |
| `:outputLoaderTemplate` | Inside `data-hs-combo-box` | string \| null | Default template | Defines a template for the loader when loading data. |
| `:preventAutoPosition` | Inside `data-hs-combo-box` | boolean | `false` | If `true`, it disables dropdown auto-positioning. |
| `:preventClientFiltering` | Inside `data-hs-combo-box` | boolean | `true` (if `:apiSearchQuery` is not empty) | If `true`, it disables client-side filtering. |
| `:keepOriginalOrder` | Inside `data-hs-combo-box` | boolean | `false` | If `true`, it keeps the original order of items. |
| `:preserveSelectionOnEmpty` | Inside `data-hs-combo-box` | boolean | `true` | If `true`, it preserves the selected value when the input is empty. |
| `data-hs-combo-box-input` | Input element | - | - | Determines which element (input) inside the initialized container will be responsible for entering data. |
| `data-hs-combo-box-output` | Output container | - | - | Determines which element inside the initialized container will be responsible for data output. |
| `data-hs-combo-box-toggle` | Toggle element | - | - | Defines an element inside the initialized container that will be responsible for hiding/showing the dropdown list. |
| `data-hs-combo-box-close` | Close element | - | - | Defines an element inside the initialized container that will be responsible for hiding the dropdown list. |
| `data-hs-combo-box-open` | Open element | - | - | Defines an element inside the initialized container that will be responsible for showing the dropdown list. |
| `data-hs-combo-box-output-item` | Item element | - | - | Defines an element inside the initialized container that will be responsible for displaying each individual data element. |
| `data-hs-combo-box-search-text` | Search text element | - | - | Defines the element within which text will be searched. |
| `data-hs-combo-box-value` | Value element | - | - | Defines an element whose text will be set as a value when selected. |
| `data-hs-combo-box-output-item-field` | Item field | string \| string[] | - | Defines a data field that will be taken to fill the element with text. |
| `data-hs-combo-box-output-item-attr` | Item attribute | - | - | Defines the attributes that will be filled with the corresponding data. For example, the src attribute of an image. |
| `:valueFrom` | Inside `data-hs-combo-box-output-item-attr` | string | - | Defines the name of the field from which data will be taken. |
| `:attr` | Inside `data-hs-combo-box-output-item-attr` | string | - | Defines the name of the attribute where the data will be supplied. |

### Tailwind Modifiers

| Name | Description |
| --- | --- |
| `hs-combo-box-active:*` | A modifier that allows you to set Tailwind classes when ComboBox has been opened. |
| `hs-combo-box-has-value:*` | A modifier that allows you to set Tailwind classes when ComboBox's input has value. |
| `hs-combo-box-selected:*` | A modifier that allows you to set Tailwind classes when `data-hs-combo-box-output-item` has been selected. |
| `hs-combo-box-tab-active:*` | A modifier that allows you to set Tailwind classes to the active group item (tab) when `groupingType` is `tabs`. |

## JavaScript API

The `HSComboBox` object is available in the global `window` object after the plugin is loaded.

### Instance Methods

These methods are called on a combobox instance.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `getCurrentData()` | None | `object \| object[]` | Returns the data of the selected element as an object. This data must first be assigned as a value to the data parameter `data-hs-combo-box-item-stored-data` when standard rendering. When rendering via the API, the data is automatically substituted into the data attribute. |
| `setCurrent()` | None | `void` | Sets the current selection based on the input value. |
| `open(val)` | `val`: `string` (optional) | `boolean` | Opens the autosuggestion list. Optionally accepts a value to set in the input. Returns `true` if opened successfully, `false` otherwise. |
| `close(val, data)` | `val`: `string \| null` (optional)<br>`data`: `object \| null` (optional) | `boolean` | Closes the autosuggestion list. Optionally accepts a value and data to set. Returns `true` if closed successfully, `false` otherwise. |
| `recalculateDirection()` | None | `void` | Forces recalculation of autosuggestion list positions. Useful after window resize or layout changes. |
| `destroy()` | None | `void` | Destroys the combobox instance, removes all generated markup, classes, and event listeners. Use when removing combobox from DOM. |

### Static Methods

These methods are called directly on the `HSComboBox` class.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `HSComboBox.getInstance(target, isInstance)` | `target`: `HTMLElement \| string` (CSS selector)<br>`isInstance`: `boolean` (optional) | `HSComboBox \| { id: string \| number, element: HSComboBox } \| null` | Returns the combobox instance associated with the target. If `isInstance` is `true`, returns collection item object `{ id, element }` where `element` is the `HSComboBox` instance. If `isInstance` is `false` or omitted, returns the `HSComboBox` instance directly. Returns `null` if combobox instance is not found. |
| `HSComboBox.autoInit()` | None | `void` | Reinitializes all ComboBoxes on the page. Useful after dynamically adding comboboxes to DOM. |
| `HSComboBox.close(target)` | `target`: `HTMLElement \| string` (CSS selector) | `void` | Closes the combobox identified by target. Accepts a DOM element or CSS selector string. |
| `HSComboBox.closeCurrentlyOpened()` | None | `void` | Closes an already open ComboBox. Useful for closing any open combobox before opening a new one. |

### Usage Examples

**Example 1: Using instance methods (public API)**
```javascript
// Create a new combobox instance
const comboBox = new HSComboBox(document.querySelector('#hs-combo-box'));
const openBtn = document.querySelector('#hs-open-btn');

// Open combobox programmatically
openBtn.addEventListener('click', () => {
  comboBox.open();
});
```

**Example 2: Getting instance and using methods (recommended pattern)**
```javascript
// Get the combobox instance
const instance = HSComboBox.getInstance('#hs-combo-box', true);

if (instance) {
  const { element } = instance; // element is the HSComboBox instance
  const openBtn = document.querySelector('#hs-open-btn');

  // Use instance methods
  openBtn.addEventListener('click', () => {
    element.open();
  });

  // Get selected data
  const selectedData = element.getCurrentData();
  console.log('Selected:', selectedData);

  // Clean up when removing from DOM
  function removeComboBox() {
    element.destroy();
  }
}
```

**Example 3: Destroying combobox instance**
```javascript
const instance = HSComboBox.getInstance('#hs-combo-box', true);

if (instance) {
  const { element } = instance;
  const removeBtn = document.querySelector('#hs-remove-btn');

  removeBtn.addEventListener('click', () => {
    // Clean up before removing from DOM
    element.destroy();
    // Now safe to remove the element
    document.querySelector('#hs-combo-box').remove();
  });
}
```

## Events

Combobox instances emit events that can be listened to for selection lifecycle hooks.

| Event Name | When Fired | Callback Parameter | Description |
| --- | --- | --- | --- |
| `on:select` | When an item is selected from the dropdown | `object` (the selected item data object) | Fires after the user clicks an option in the combobox dropdown. |

### Event Usage Example

```javascript
// Get combobox instance
const instance = HSCombobox.getInstance('#hs-combobox', true);

if (instance) {
  const { element } = instance;

  // Listen for item selection
  element.on('select', (data) => {
    console.log('Selected item:', data);
    // Use selected data to update other parts of the UI
  });
}
```

## Common Patterns

### Pattern 1: API-based ComboBox

Fetch suggestions from an API endpoint.

```html
<div data-hs-combo-box='{
  "apiUrl": "https://api.example.com/search",
  "apiSearchQuery": "q",
  "minSearchLength": 2
}'>
  <input data-hs-combo-box-input />
  <div data-hs-combo-box-output></div>
</div>
```

### Pattern 2: Programmatic Control

Control combobox from external buttons.

```html
<div id="hs-combo-box-first" data-hs-combo-box>
  <input data-hs-combo-box-input />
  <div data-hs-combo-box-output></div>
</div>

<button id="hs-open-combo-box">Open</button>
<button id="hs-close-combo-box">Close</button>

<script>
  const instance = HSComboBox.getInstance('#hs-combo-box-first', true);
  
  if (instance) {
    const { element } = instance;

    document.querySelector('#hs-open-combo-box').addEventListener('click', () => {
      element.open();
    });

    document.querySelector('#hs-close-combo-box').addEventListener('click', () => {
      element.close();
    });
  }
</script>
```

## License

Copyright (c) 2024 Htmlstream.

Licensed under the [MIT License](https://opensource.org/licenses/MIT).
