Combo editor (autocomplete)
A text box with a list that filters as you type: pick with the arrows, allow free text or only listed values, and search a server with an async options function.
Live demoCombo editor
Filter as you type, listed values only, and a server search.
Code for this demo
import { createGrid, type ColumnDef, type Row } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, separator, text, tr, type DemoContext } from '../ui';
const CITIES = ['Seoul', 'Busan', 'Incheon', 'Daegu', 'Daejeon', 'Gwangju', 'Ulsan', 'Suwon', 'Tokyo', 'Osaka', 'Paris', 'Berlin', 'Madrid', 'London', 'New York'];
const KO: Record<string, string> = { Seoul: '서울', Busan: '부산', Incheon: '인천', Daegu: '대구', Daejeon: '대전', Gwangju: '광주', Ulsan: '울산', Suwon: '수원', Tokyo: '도쿄', Osaka: '오사카', Paris: '파리', Berlin: '베를린', Madrid: '마드리드', London: '런던', 'New York': '뉴욕' };
export default function mount(ctx: DemoContext) {
const h = (en: string, ko: string) => tr(ctx.lang, { en, ko });
const options = CITIES.map(c => ({ value: c, label: ctx.lang === 'ko' ? KO[c] : c }));
// a fake server: answers after 300 ms with the matching cities
const search = (_row: Row, q: string) => new Promise<typeof options>(res => setTimeout(() => res(options.filter(o => o.label.toLowerCase().includes(q.toLowerCase()))), 300));
const data = makeRows(200).map((r, i) => ({ ...r, city: CITIES[i % CITIES.length], hub: CITIES[(i * 3) % CITIES.length], remote: CITIES[(i * 7) % CITIES.length] }));
const grid = createGrid(ctx.host, {
columns: [
{ field: 'name', header: h('Name', '이름'), width: 150 },
{ field: 'city', header: h('City (free text)', '도시(자유 입력)'), width: 150, editable: true, editor: { type: 'combo', options } },
{ field: 'hub', header: h('Hub (listed only)', '거점(목록만)'), width: 150, editable: true, editor: { type: 'combo', options, free: false } },
{ field: 'remote', header: h('Server search', '서버 검색'), width: 150, editable: true, editor: { type: 'combo', options: search, free: false } },
],
data, locale: ctx.lang,
});
grid.on('cellValueChanged', e => ctx.log(`${e.colId} = ${String(e.newValue)}`));
grid.on('validationFailed', e => ctx.log(`refused: ${String(e.value ?? '')} — ${e.message}`));
text(ctx, h('Type to filter (ㅅㅇ works in Korean), ↓ ↑ to pick, Enter to save. The hub column refuses cities that are not listed.',
'입력하면 걸러지고(초성 ㅅㅇ도 됨) ↓ ↑로 고르고 Enter로 저장합니다. 거점 컬럼은 목록에 없는 도시를 받지 않습니다.'));
return grid;
}editor: { type: 'combo' } is a text box with a list under it that filters as you type. It suits long lists (cities, products, customers) where a plain select is hard to scroll. Matching ignores case and also understands Korean initial consonants (ㅅㅇ finds 서울).
Basic use
const cities = [{ value: 'SEL', label: 'Seoul' }, { value: 'BSN', label: 'Busan' }, { value: 'ICN', label: 'Incheon' }];
createGrid(el, {
columns: [
// free text allowed (the default): a city not in the list is kept as typed
{ field: 'city', editable: true, editor: { type: 'combo', options: cities } },
// only listed values: anything else is refused with a message
{ field: 'hub', editable: true, editor: { type: 'combo', options: cities, free: false } },
],
data,
});Keyboard and mouse
| Key | Does |
|---|---|
| Type | Filters the list (label or value contains the text) |
| ↓ / ↑ | Move the highlight; the box shows the highlighted option |
| Enter | Save the highlighted option, or the typed text (free) — with free: false the first match is highlighted, so Enter after a few letters picks it |
| Click an item | Save it at once |
| Esc | Close without saving |
Options
| Name | Type | Description |
|---|---|---|
options | SelectOption[] | (row, query) => SelectOption[] | Promise<SelectOption[]> | The choices: values, or { value, label, disabled }. A function receives the row and the typed text; it is called again as people type (150 ms after the last key) and may return a Promise. While a Promise is pending, the list says Loading…. |
free | boolean | Accept text that is not in the list (default true). With false, only listed, enabled values are saved; pasting other text is skipped. |
Searching a server
{ field: 'customerId', header: 'Customer', editable: true,
editor: {
type: 'combo',
free: false,
// called with what is typed; answers that arrive late for an older query are dropped
options: async (row, query) => {
const res = await fetch('/api/customers?q=' + encodeURIComponent(query));
const list = await res.json();
return list.map(c => ({ value: c.id, label: c.name }));
},
},
// show the name for the stored id (the cell keeps the id)
valueGetter: row => row.customerName ?? row.customerId,
}With a function, the grid cannot know every option in advance: the cell shows the stored value (use valueGetter or renderer to show a name), and with free: false pasted text is refused because it cannot be checked.
What the cell shows and stores
- With a fixed list, the cell shows the option label and stores its value (
SELis stored, Seoul is shown). - Pasting accepts a label or a value, in any case.
- A combo with
free: falseand a fixed list gets a list filter (set filter) by label; with free text it gets a text filter. See Filters on editor columns.
Questions about this page
How do I make an autocomplete cell that searches the server?
Give the combo an options function: editor: { type: 'combo', options: (row, q) => fetch('/api?q=' + q).then(r => r.json()) }. It is called as people type (debounced) and may return a Promise.