Data

Find and Replace

Find and replace across the rows a filter left. It honours case and whole-cell, and counts what it can write apart from what it found.

Basic Usage

Type in the panel below and the matches light up in the grid, with the one under the cursor marked more strongly. Ctrl/Cmd+F opens the panel on the find field and Ctrl/Cmd+H opens it on the replace one. While the cursor is in either field, Enter and Shift+Enter step through the matches and Escape closes the panel.

0 found, 0 writable
Member
Email
Team
Role
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
Mai Nguyen
mai.nguyen7@example.com
Support
Analyst
Bruno Novak
bruno.novak8@example.com
Support
Analyst
Grace Dubois
grace.dubois9@example.com
Support
Manager
Tuan Ivanov
tuan.ivanov10@example.com
Design
Engineer
Bruno Andersen
bruno.andersen11@example.com
Growth
Support
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Designer
Grace Costa
grace.costa13@example.com
Platform
Support
Niko Muller
niko.muller14@example.com
Core
Manager
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Analyst
Dara Okafor
dara.okafor16@example.com
Platform
Support
Quyen Silva
quyen.silva17@example.com
Design
Manager
Grace Haddad
grace.haddad18@example.com
Support
Designer
Keiko Novak
keiko.novak19@example.com
Core
Analyst
Tuan Dubois
tuan.dubois20@example.com
Growth
Engineer
Chi Kowalski
chi.kowalski21@example.com
Support
Analyst
Chi Silva
chi.silva22@example.com
Design
Designer
Lucas Muller
lucas.muller23@example.com
Core
Support
Hoang Rossi
hoang.rossi24@example.com
Core
Support
24 rows
<script lang="ts">
  import {
    createDataGrid, DataGrid, editing, filtering, findReplace, Grid
  } from '@sv5ui/datagrid';

  const grid = createDataGrid<Person>({
    columns, data, getRowId,
    features: [
      filtering(),
      editing(),        // without it, find works and replace has nothing to write with
      findReplace()
    ]
  });
</script>

<Grid.FindReplace {grid} />
<DataGrid {grid} class="h-96" />

<!-- Ctrl+F opens it, Enter and Shift+Enter step through the matches, and
     Escape closes it. Turn the shortcuts off with hotkeys: false. -->

It Searches the Rows a Filter Left

Not the whole data array. Narrow the grid to one team below and the same query finds fewer matches, because what a reader is looking at is what they mean to search.

Filter to a team:

Query (empty) found 0 matches in the whole set.

Member
Email
Team
Role
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
Mai Nguyen
mai.nguyen7@example.com
Support
Analyst
Bruno Novak
bruno.novak8@example.com
Support
Analyst
Grace Dubois
grace.dubois9@example.com
Support
Manager
Tuan Ivanov
tuan.ivanov10@example.com
Design
Engineer
Bruno Andersen
bruno.andersen11@example.com
Growth
Support
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Designer
Grace Costa
grace.costa13@example.com
Platform
Support
Niko Muller
niko.muller14@example.com
Core
Manager
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Analyst
Dara Okafor
dara.okafor16@example.com
Platform
Support
Quyen Silva
quyen.silva17@example.com
Design
Manager
Grace Haddad
grace.haddad18@example.com
Support
Designer
Keiko Novak
keiko.novak19@example.com
Core
Analyst
Tuan Dubois
tuan.dubois20@example.com
Growth
Engineer
Chi Kowalski
chi.kowalski21@example.com
Support
Analyst
Chi Silva
chi.silva22@example.com
Design
Designer
Lucas Muller
lucas.muller23@example.com
Core
Support
Hoang Rossi
hoang.rossi24@example.com
Core
Support
24 rows
const find = getFindReplace(grid)!;
find.query = 'a';
find.total;   // 60 over the whole set

grid.api.setQuickFilter?.('Core');
find.total;   // 13 over the rows the filter left

// It matches what a cell DRAWS, not the value behind it: a date matches its
// formatted text, and a masked column is searched masked.

Found Is Not the Same as Writable

Only Member and Email are editable in this grid, so a query that also hits Team or Role finds more cells than it can write. The panel reports both numbers rather than one, so a replace that skips half the matches does not come as a surprise.

0 found 0 writable

Core lives in the Team column, which is not editable: found, not writable. example lives in Email, which is: found and writable.

Member
Email
Team
Role
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
Mai Nguyen
mai.nguyen7@example.com
Support
Analyst
Bruno Novak
bruno.novak8@example.com
Support
Analyst
Grace Dubois
grace.dubois9@example.com
Support
Manager
Tuan Ivanov
tuan.ivanov10@example.com
Design
Engineer
Bruno Andersen
bruno.andersen11@example.com
Growth
Support
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Designer
Grace Costa
grace.costa13@example.com
Platform
Support
Niko Muller
niko.muller14@example.com
Core
Manager
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Analyst
Dara Okafor
dara.okafor16@example.com
Platform
Support
Quyen Silva
quyen.silva17@example.com
Design
Manager
Grace Haddad
grace.haddad18@example.com
Support
Designer
Keiko Novak
keiko.novak19@example.com
Core
Analyst
Tuan Dubois
tuan.dubois20@example.com
Growth
Engineer
Chi Kowalski
chi.kowalski21@example.com
Support
Analyst
Chi Silva
chi.silva22@example.com
Design
Designer
Lucas Muller
lucas.muller23@example.com
Core
Support
Hoang Rossi
hoang.rossi24@example.com
Core
Support
24 rows
const find = getFindReplace(grid)!;

find.total;           // how many cells matched
find.writableTotal;   // how many of those can actually be written
find.current;         // which match the cursor is on, zero-based
find.active;          // that Match: { row, col, rowId, columnId }
find.matches;         // all of them

// The two counts differ, and the difference is the point. A match in a column
// that is not editable, or one masked by policy(), is FOUND but not
// REPLACEABLE: the value on screen is not the value underneath, so writing
// over it would commit the mask.

find.canReplaceCurrent;   // false on a match that cannot be written
find.canReplaceAll;       // false when nothing found is writable
find.replaceAvailable;    // false when the feature was registered replace: false

Under a Server Row Model

The grid holds one page, so a search runs over that page and partial says so: the count is of what is loaded rather than of the set.

Replacing is refused unless you opt in, and the default is the safe one. A replace writes into the page the grid is holding. The next fetch overwrites it, and the panel has already said it succeeded, so ten replaced cells can silently go back to what the server still has. Turn serverReplace on only when your edits reach whatever getRows reads from, which is something only your app knows.

The grid below is that situation rather than a description of it: 5,000 people on the server, one block of 100 in the browser. Search it, then scroll to load another block and search again. The count moves, because what a search can see is what has arrived.

? rows on the server 0 loaded here partial: true 0 found, 0 writable replaceAvailable: false
Member
Email
Team
Role

No data

0 rows

The last few requests

  • Nothing yet. Scroll the grid.
find.partial;            // true: the grid holds one page, not the set
find.replaceAvailable;   // false unless serverReplace is set

// Off by default, and the default is the safe one. A replace writes into the
// page the grid is holding; the next fetch overwrites it, and the panel has
// already reported success. Turn it on only when your edits reach whatever
// getRows reads from.
findReplace({ serverReplace: true });

What the State Reports

From getFindReplace(grid). The two counts are separate on purpose.

MemberWhat it answers
totalHow many cells matched
writableTotalHow many of those can be written. A match in a column that is not editable, or one a policy masks, is found but not replaceable
current / activeWhich match the cursor is on, and that Match itself
matchesAll of them, as { row, col, rowId, columnId }
partialTrue when the grid does not hold every row, so the count is of a page rather than a set
canReplaceCurrent / canReplaceAllWhether the buttons should be offered at all
replacedHow many cells the last replace wrote, or null if none has run

Options

OptionDescription
replaceOffer the replace field. false gives a find-only panel
hotkeysCtrl/Cmd+F to open on the find field, and Ctrl/Cmd+H on the replace one when replace is on. The listener is on window and calls preventDefault, so those two keys belong to the grid while the panel is mounted, which is what a grid-wide find has to do to be reachable before the user has clicked into the grid. Enter, Shift+Enter and Escape are not covered by this: they are bound to the two fields themselves and keep working with hotkeys off
serverReplaceAllow replacing under a server row model. Off by default because a replace writes into the loaded page and the next fetch overwrites it, after the panel has reported success

Driving It From Code

import { getFindReplace } from '@sv5ui/datagrid';

const find = getFindReplace(grid)!;

find.show();
find.hide();
find.query = 'ada';
find.replacement = 'Ada';
find.step(1);    // next match
find.step(-1);   // previous

await find.replaceCurrent();
await find.replaceAll();
find.replaced;   // how many cells the last replace wrote, or null

// And on grid.api:
grid.api.openFind?.();
grid.api.findNext?.();

// A replace goes through the edit stack, so one undo takes back a replaceAll
// in full rather than one cell at a time.