@xbenjii/pg-typeahead
v1.0.8
Published

Readme
pg-typeahead
A lightweight, framework-free address typeahead widget with built-in debouncing, keyboard navigation, and ARIA accessibility. Enhances an existing <input> element — works in any framework via mount(), or use AddressTypeahead and AddressSearchController directly.
Install
npm install @xbenjii/pg-typeaheadNo peer dependencies — the library is pure vanilla TypeScript.
CDN
Use the UMD build directly from a script tag (exposes window.PgTypeahead):
<link rel="stylesheet" href="https://cdn.prestigegifting.co.uk/packages/[email protected]/style.css" />
<script src="https://cdn.prestigegifting.co.uk/packages/[email protected]/index.umd.js"></script>
<input id="address" type="text" />
<script>
var provider = new PgTypeahead.RestAddressProvider({
baseUrl: 'https://api.example.com',
headers: { 'x-api-key': 'YOUR_KEY' },
});
PgTypeahead.mount(document.getElementById('address'), {
provider: provider,
onSelect: function (suggestion) {
console.log(suggestion);
},
});
</script>Or use the ES module build:
<link rel="stylesheet" href="https://cdn.prestigegifting.co.uk/packages/[email protected]/style.css" />
<input id="address" type="text" />
<script type="module">
import { mount, RestAddressProvider } from 'https://cdn.prestigegifting.co.uk/packages/[email protected]/index.js';
const provider = new RestAddressProvider({
baseUrl: 'https://api.example.com',
});
mount(document.getElementById('address'), {
provider,
onSelect: (s) => console.log(s),
});
</script>Quick Start
Using mount()
The simplest way to add the widget to any page or framework:
import { mount, RestAddressProvider } from '@xbenjii/pg-typeahead';
import '@xbenjii/pg-typeahead/style.css';
const provider = new RestAddressProvider({
baseUrl: 'https://api.example.com',
headers: { 'x-api-key': 'YOUR_KEY' },
});
const instance = mount(document.querySelector('#address'), {
provider,
onSelect: (suggestion) => console.log(suggestion),
});
// Update options at any time
instance.update({ disabled: true });
// Clean up — restores the original input
instance.destroy();Using AddressTypeahead directly
For more control, instantiate the widget class yourself:
import { AddressTypeahead, RestAddressProvider } from '@xbenjii/pg-typeahead';
import '@xbenjii/pg-typeahead/style.css';
const provider = new RestAddressProvider({
baseUrl: 'https://api.example.com',
});
const input = document.querySelector<HTMLInputElement>('#address')!;
const widget = new AddressTypeahead(input, {
provider,
onSelect: (s) => console.log(s),
});
// Later
widget.update({ placeholder: 'New placeholder…' });
widget.destroy();Headless (controller only)
Use AddressSearchController to handle debounced search logic with your own UI:
import { AddressSearchController, RestAddressProvider } from '@xbenjii/pg-typeahead';
const provider = new RestAddressProvider({
baseUrl: 'https://api.example.com',
});
const ctrl = new AddressSearchController({
provider,
debounceMs: 300,
minChars: 2,
onUpdate: () => {
console.log(ctrl.suggestions); // AddressSuggestion[]
console.log(ctrl.isLoading); // boolean
console.log(ctrl.error); // Error | null
},
});
ctrl.setQuery('123 Main');
// Reset
ctrl.clear();
// Clean up
ctrl.destroy();Providers
RestAddressProvider
Connects to any JSON REST API out of the box.
import { RestAddressProvider } from '@xbenjii/pg-typeahead';
const provider = new RestAddressProvider({
baseUrl: 'https://api.example.com',
searchPath: '/autocomplete', // default: "/search"
detailsPath: '/place', // default: "/details"
headers: { Authorization: 'Bearer token' },
transformResults: (data) => { // map your API shape → AddressSuggestion[]
return (data as any).results.map((r: any) => ({
id: r.place_id,
primaryText: r.street,
secondaryText: `${r.city}, ${r.state} ${r.zip}`,
description: r.formatted,
}));
},
});Custom URL format
Use searchUrl (and optionally detailsUrl) when the API uses a non-standard URL shape. Pass a template string with a {query} placeholder, or a function for full control:
// Template string — {query} is replaced with the percent-encoded search text
const provider = new RestAddressProvider({
searchUrl: 'https://api.example.com/postcode/{query}',
});
// Function — build the URL yourself
const provider = new RestAddressProvider({
searchUrl: (q) => `https://api.example.com/find?term=${encodeURIComponent(q)}&limit=5`,
detailsUrl: (id) => `https://api.example.com/place/${encodeURIComponent(id)}`,
});When searchUrl or detailsUrl is provided it takes precedence over baseUrl + searchPath/detailsPath.
Custom Provider
Implement the AddressProvider interface to use any data source:
import type { AddressProvider, AddressSuggestion } from '@xbenjii/pg-typeahead';
class MyProvider implements AddressProvider {
async search(query: string, signal?: AbortSignal): Promise<AddressSuggestion[]> {
// your logic here
}
async getDetails(id: string, signal?: AbortSignal): Promise<AddressSuggestion> {
// optional — resolve full details for a selected suggestion
}
}Styling
Default styles
Import the bundled CSS for sensible defaults:
import '@xbenjii/pg-typeahead/style.css';All classes use a .pg-typeahead__* BEM convention and low specificity, so they're easy to override.
Custom class names
Pass a classNames object to replace any or all default classes:
mount(element, {
provider,
classNames: {
root: 'my-root',
input: 'my-input',
listbox: 'my-dropdown',
option: 'my-option',
optionHighlighted: 'my-option--active',
loading: 'my-loading',
noResults: 'my-empty',
},
});Custom classes are merged with the defaults, so you can override selectively.
API Reference
mount(input, options) → TypeaheadInstance
Enhances an existing <input> element with typeahead behaviour and returns a handle for controlling it. The input is wrapped in a positioned container; calling destroy() restores it to its original state.
MountOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| provider | AddressProvider | required | The address lookup provider |
| debounceMs | number | 250 | Debounce delay before searching |
| minChars | number | 3 | Minimum input length to trigger search |
| placeholder | string | "Search for an address…" | Input placeholder text |
| onSelect | (s: AddressSuggestion) => void | — | Called when a suggestion is selected |
| onChange | (value: string) => void | — | Called on every input change |
| displayValue | (s: AddressSuggestion) => string | s => s.description | Derive the string written into the input on selection |
| classNames | TypeaheadClassNames | — | Override CSS class names |
| ariaLabel | string | "Address search" | Accessible label for the input |
| value | string | — | Initial value of the input |
| disabled | boolean | false | Disable the component |
TypeaheadInstance
| Method | Description |
|--------|-------------|
| update(opts) | Re-render with new partial MountOptions |
| destroy() | Unwrap the input and clean up |
AddressSearchController
Framework-agnostic controller that handles debounced address lookup. Use this to build a completely custom UI.
Constructor options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| provider | AddressProvider | required | The address lookup provider |
| debounceMs | number | 250 | Debounce interval in ms |
| minChars | number | 3 | Minimum query length before searching |
| onUpdate | () => void | required | Called whenever internal state changes |
Properties
| Property | Type | Description |
|----------|------|-------------|
| query | string | Current search query |
| suggestions | AddressSuggestion[] | Current results |
| isLoading | boolean | Whether a search is in flight |
| error | Error \| null | Last error, if any |
Methods
| Method | Description |
|--------|-------------|
| setQuery(value) | Update the query and trigger a debounced search |
| clear() | Reset all state |
| destroy() | Cancel pending requests and timers |
AddressSuggestion
{
id: string;
primaryText: string;
secondaryText?: string;
description: string;
structured?: {
line1?: string;
line2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
}RestProviderOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseUrl | string | — | Base URL of the address API |
| searchPath | string | "/search" | Path appended for search requests |
| detailsPath | string | "/details" | Path appended for detail requests |
| searchUrl | string \| (query: string) => string | — | Custom search URL template or function (overrides baseUrl + searchPath) |
| detailsUrl | string \| (id: string) => string | — | Custom details URL template or function (overrides baseUrl + detailsPath) |
| headers | Record<string, string> | — | Extra headers (e.g. API keys) |
| transformResults | (data: unknown) => AddressSuggestion[] | — | Map custom API responses to suggestions |
| transformDetails | (data: unknown) => AddressSuggestion | — | Map custom detail responses |
CSS Classes Reference
| Class | Element |
|-------|---------|
| .pg-typeahead | Root wrapper |
| .pg-typeahead__input | The text input |
| .pg-typeahead__listbox | Dropdown suggestions list |
| .pg-typeahead__option | Each suggestion item |
| .pg-typeahead__option--highlighted | Keyboard/hover highlighted item |
| .pg-typeahead__loading | Loading state message |
| .pg-typeahead__no-results | Empty results message |
| .pg-typeahead__primary | Primary text inside an option |
| .pg-typeahead__secondary | Secondary text inside an option |
Development
npm run dev # Start example app at localhost:5173
npm run build # Build the library
npm run lint # Run ESLintLicense
MIT
