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.
<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.
Query (empty) found 0 matches in the
whole set.
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.
Core lives in the Team column, which is not editable: found, not writable. example lives in Email, which is: found and writable.
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: falseUnder 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.
No data
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.
| Member | What it answers |
|---|---|
total | How many cells matched |
writableTotal | How 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 / active | Which match the cursor is on, and that Match itself |
matches | All of them, as { row, col, rowId, columnId } |
partial | True when the grid does not hold every row, so the count is of a page rather than a set |
canReplaceCurrent / canReplaceAll | Whether the buttons should be offered at all |
replaced | How many cells the last replace wrote, or null if none has run |
Options
| Option | Description |
|---|---|
replace | Offer the replace field. false gives a find-only panel |
hotkeys | Ctrl/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 |
serverReplace | Allow 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.