Idea of Undo/redo feature
- An Undo/Redo feature isn't implemented internally, but we prepared an example which you can follow.
- ReactGrid bypasses undo and redo events.
- You can handle
onKeyDown
event to use your own implemented Undo/Redo feature. - The
ReactGrid
component receives revoked or restored data and gets rerendered.
It's possible to use with Ctrl + z and Ctrl + y keyboard shortcuts or prepared by you buttons.
When are my handler functions called?
- Changes have been committed after Ctrl + z and Ctrl + y key.
- Changes have been committed after
Undo
andRedo
button.
Implementing feature in your project
This feature develops handling changes example.
Let's start with updating imports:
import {
ReactGrid,
Column,
Row,
CellChange,
TextCell,
} from "@silevis/reactgrid";
Sample Undo/Redo feature
For this reason we prepared special functions, which you can use in your project and adjust to it. We have an array of changes made by the user at each step. The place in the history of changes, where you currently are, is stored. Look at this function:
const applyNewValue = (
changes: CellChange<TextCell>[],
prevPeople: Person[],
usePrevValue: boolean = false
): Person[] => {
changes.forEach((change) => {
const personIndex = change.rowId;
const fieldName = change.columnId;
const cell = usePrevValue ? change.previousCell : change.newCell;
prevPeople[personIndex][fieldName] = cell.text;
});
return [...prevPeople];
};
Function applyChangesToPeople
is a similar function which is responsible for handling changes.
Additionally we need to store all the changes and the index of changes, which will help us to move within the changes array.
const applyChangesToPeople = (
changes: CellChange<TextCell>[],
prevPeople: Person[]
): Person[] => {
const updated = applyNewValue(changes, prevPeople);
setCellChanges([...cellChanges.slice(0, cellChangesIndex + 1), changes]);
setCellChangesIndex(cellChangesIndex + 1);
return updated;
};
The undoChanges
function allows you to set the previous value and change the index in the appropriate place in the array.
The change index is moved one step backwards.
const undoChanges = (
changes: CellChange<TextCell>[],
prevPeople: Person[]
): Person[] => {
const updated = applyNewValue(changes, prevPeople, true);
setCellChangesIndex(cellChangesIndex - 1);
return updated;
};
The redoChanges
function allows you to set the restored change. Change index also has to be moved one position forward.
const redoChanges = (
changes: CellChange<TextCell>[],
prevPeople: Person[]
): Person[] => {
const updated = applyNewValue(changes, prevPeople);
setCellChangesIndex(cellChangesIndex + 1);
return updated;
};
Let's use the above functions inside an example App
component. We added two hooks inside it:
const [cellChangesIndex, setCellChangesIndex] = React.useState(() => -1);
const [cellChanges, setCellChanges] = React.useState<CellChange<TextCell>[][]>(
() => []
);
The first one stores current index of changes for the second hook - storing an array of changes that has affected
the TextCells
.
We have wrapped ReactGrid with div
element that listens to onKeyDown
event.
Changes can be also restored/redone with two buttons.
Inside both methods (handleUndoChanges
and handleRedoChanges
) we update our data - poople array.
These changes come from cellChanges
array.
function App() {
const [people, setPeople] = React.useState<Person[]>(getPeople());
const [cellChangesIndex, setCellChangesIndex] = React.useState(() => -1);
const [cellChanges, setCellChanges] = React.useState<
CellChange<TextCell>[][]
>(() => []);
const rows = getRows(people);
const columns = getColumns();
const handleChanges = (changes: CellChange<TextCell>[]) => {
setPeople((prevPeople) => applyChangesToPeople(changes, prevPeople));
};
const handleUndoChanges = () => {
if (cellChangesIndex >= 0) {
setPeople((prevPeople) =>
undoChanges(cellChanges[cellChangesIndex], prevPeople)
);
}
};
const handleRedoChanges = () => {
if (cellChangesIndex + 1 <= cellChanges.length - 1) {
setPeople((prevPeople) =>
redoChanges(cellChanges[cellChangesIndex + 1], prevPeople)
);
}
};
return (
<div
onKeyDown={(e) => {
if ((!isMacOs() && e.ctrlKey) || e.metaKey) {
switch (e.key) {
case "z":
handleUndoChanges();
return;
case "y":
handleRedoChanges();
return;
}
}
}}
>
<ReactGrid rows={rows} columns={columns} onCellsChanged={handleChanges} />
<button onClick={handleUndoChanges}>Undo</button>
<button onClick={handleRedoChanges}>Redo</button>
</div>
);
}
Live demo
And here's an interactive demo showing the Undo / Redo feature.
Code
interface Person { name: string; surname: string; } const getPeople = (): Person[] => [ { name: "Thomas", surname: "Goldman" }, { name: "Susie", surname: "Quattro" }, { name: "", surname: "" } ]; const getColumns = (): Column[] => [ { columnId: "name", width: 150 }, { columnId: "surname", width: 150 } ]; const headerRow: Row = { rowId: "header", cells: [ { type: "header", text: "Name" }, { type: "header", text: "Surname" } ] }; const getRows = (people: Person[]): Row[] => [ headerRow, ...people.map<Row>((person, idx) => ({ rowId: idx, cells: [ { type: "text", text: person.name }, { type: "text", text: person.surname } ] })) ]; const isMacOs = () => window.navigator.appVersion.indexOf("Mac") !== -1; function App() { const [people, setPeople] = React.useState<Person[]>(getPeople()); const [cellChangesIndex, setCellChangesIndex] = React.useState(() => -1); const [cellChanges, setCellChanges] = React.useState< CellChange<TextCell>[][] > (() => []); const applyNewValue = ( changes: CellChange<TextCell>[], prevPeople: Person[], usePrevValue: boolean = false ): Person[] => { changes.forEach((change) => { const personIndex = change.rowId; const fieldName = change.columnId; const cell = usePrevValue ? change.previousCell : change.newCell; prevPeople[personIndex][fieldName] = cell.text; }); return [...prevPeople]; }; const applyChangesToPeople = ( changes: CellChange<TextCell>[], prevPeople: Person[] ): Person[] => { const updated = applyNewValue(changes, prevPeople); setCellChanges([...cellChanges.slice(0, cellChangesIndex + 1), changes]); setCellChangesIndex(cellChangesIndex + 1); return updated; }; const rows = getRows(people); const columns = getColumns(); const undoChanges = ( changes: CellChange<TextCell>[], prevPeople: Person[] ): Person[] => { const updated = applyNewValue(changes, prevPeople, true); setCellChangesIndex(cellChangesIndex - 1); return updated; }; const redoChanges = ( changes: CellChange<TextCell>[], prevPeople: Person[] ): Person[] => { const updated = applyNewValue(changes, prevPeople); setCellChangesIndex(cellChangesIndex + 1); return updated; }; const handleChanges = (changes: CellChange<TextCell>[]) => { setPeople((prevPeople) => applyChangesToPeople(changes, prevPeople)); }; const handleUndoChanges = () => { if (cellChangesIndex >= 0) { setPeople((prevPeople) => undoChanges(cellChanges[cellChangesIndex], prevPeople) ); } }; const handleRedoChanges = () => { if (cellChangesIndex + 1 <= cellChanges.length - 1) { setPeople((prevPeople) => redoChanges(cellChanges[cellChangesIndex + 1], prevPeople) ); } }; return ( <div onKeyDown={(e) => { if ((!isMacOs() && e.ctrlKey) || e.metaKey) { switch (e.key) { case "z": handleUndoChanges(); return; case "y": handleRedoChanges(); return; } } }} > <ReactGrid rows={rows} columns={columns} onCellsChanged={handleChanges} /> <button onClick={handleUndoChanges}>Undo</button> <button onClick={handleRedoChanges}>Redo</button> </div> ); } render(<App />, document.getElementById("root"));
Preview