developer guide
Find and Search in a JavaScript Data Grid
Find highlights every match and walks the reader from one to the next, leaving the rows around it in place, which is what filtering cannot do. grid.find runs the same search from your own code, with a count, the current position and the matches themselves.
Developer guide › Sorting, filtering and find › Find and Search in a JavaScript Data Grid
Find
The quick filter answers "show me only the rows that contain X". Find answers a different question - "where is X?" - and leaves every other row exactly where it was, so you keep your place and the neighbours that give a value its meaning. Press Ctrl+F (Cmd+F on a Mac) with focus in the grid: a bar opens above the header with focus in its input, every cell whose displayed text matches lights up in place, the bar reads "N of M", Enter and Shift+Enter step through the matches with wrap, and Escape closes it and clears the marks.
The same thing from code
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const grid = createHeadlessGrid({
rowKey: 'id',
find: { shortcut: true, debounce: 0 }, // the bar's settings; `find: false` removes the bar
columns: [
{ field: 'name' },
{ field: 'price', format: (p) => `$${p.value.toFixed(2)}` },
],
rows: [
{ id: 'a', name: 'Acme', price: 3.5 },
{ id: 'b', name: 'Beta', price: 13.25 },
{ id: 'c', name: 'Acme Two', price: 3.75 },
],
});
let events = 0;
grid.on('find:changed', () => { events += 1; });
const count = grid.find('$3.'); // the formatted text: two prices begin "$3."
const first = grid.find.current(); // { key: 'a', colId: 'price', index: 0, pinned: null }
grid.find.next(); // row c; next() again wraps back to a
const rowsStillShown = grid.rows.count(); // 3 - find never removes a row
grid.destroy();
return `${count.total} matches, ${rowsStillShown} rows shown, current ${first.key}, events ${events > 0}`;
grid.find(text, opts) searches now and returns a FindCount; it is
callable like grid.highlight. The options are a FindQuery:
caseSensitive, wholeCell, columns (an id or a list of
ids; omitted searches every visible column) and from (the display index the
first current match is chosen at or after). Then find.next(),
find.prev() and find.goTo(i) move the current match and scroll it
into view; find.matches(), find.count(),
find.current() and find.state() read the result;
find.open(text?), find.close() and find.clear() drive
the bar; find.stateFor(key, colId) is what the painter asks. Every change fires
find:changed with the query, the open flag and the count.
| Rule | What it means |
|---|---|
| Display text | Find matches what the cell shows - a column format, a unit type, a lookup label - never the raw value. Searching $3. finds prices formatted that way. There is no regular-expression mode; the quick filter has one. |
| An overlay, not a filter | No row is reordered, removed or edited. Matches are painted as .lat-cell--find, the current one also as .lat-cell--find-current, coloured by --lattice-find-match and --lattice-find-current. Find and the quick filter coexist: both may be active, and find re-runs over whatever the filter leaves. |
| Pinned rows and columns | Rows pinned to either edge (and a bottom grand total) are searched and painted like any other; a pinned match has index: -1 and pinned: 'top' | 'bottom'. Pinned columns are cells like any other. |
| Virtualised rows | Matches are computed from the row model, not the DOM, so a match five thousand rows down is counted without rendering it; stepping to it scrolls it into view, and the paint follows the render. |
| The active cell | Stepping to a match makes it the active cell, so Enter in the grid edits it - except while an edit is already open, when the match is scrolled and painted and the editor is left alone. |
| Windowed sources | A paged pushdown source (OData, DuckDB, REST) holds only its loaded rows client-side, so only those are searched. The count says so - "N of M in loaded rows", and FindCount.windowed is true with loaded and rows beside it - rather than presenting a page-one count as the whole. Pushing find to the adapter is a follow-up, not a v1 promise. |
| Large grids | Typing is scanned in per-frame slices from the row at the top of the viewport, so the matches on screen appear after the first slice and the grid stays interactive; the count reads "N of M so far" and count.complete is false until the scan finishes. grid.find(text) scans to completion before returning, so its answer is final. |
| The browser's find | Ctrl+F is claimed only while focus is inside the grid and not in a text field, so the page's own find works everywhere else and an open cell editor keeps it. find: { shortcut: false } leaves the binding to the page and keeps the bar reachable through find.open(); find: false removes the bar altogether. |
| Accessibility | The bar is a role="search" landmark; every control is a native input, button or select with a catalogue name, so nothing needs a mouse. The count is announced through a polite role="status" line once per completed search ("3 of 12 matches", "No matches in loaded rows"); the current match becomes the focused cell when no edit is open, so a screen reader reads it. The strings are in every bundled locale. |
| Keys in the bar | Only Escape and Enter in the input are consumed by the bar. Everything else - Tab between its controls, Enter and Space on its buttons, a page's own Ctrl+S - propagates as it would from any form control, so a host's document-level shortcuts still see it; the grid's own keyboard and range layers stand aside for a key aimed at the bar, which is what keeps Tab from being read as "next cell" and Enter from opening an editor. |
| Pinned strips | Stepping to a match scrolls it fully into the part of the body the pinned strips do not cover - below pinned-top rows and sticky group headings, above pinned-bottom rows and a bottom grand total. That is grid.scroll.toRow's behaviour for every caller, not only find. |
Configuration
find: false // no bar, no Ctrl+F; grid.find(text) still works
find: { shortcut: false } // bar via grid.find.open() only
find: { debounce: 250 } // a slower typist, or a slower grid
grid.find('acme', { caseSensitive: true, wholeCell: false, columns: ['customer'] });
grid.find.count(); // { current, total, complete, windowed, loaded, rows }
grid.on('find:changed', (e) => status.textContent = `${e.count.current} of ${e.count.total}`);