Lattice Grid Buy a licence

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 guideSorting, 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.

RuleWhat it means
Display textFind 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 filterNo 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 columnsRows 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 rowsMatches 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 cellStepping 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 sourcesA 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 gridsTyping 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 findCtrl+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.
AccessibilityThe 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 barOnly 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 stripsStepping 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}`);