Last active
October 14, 2024 01:43
-
-
Save 1j01/bd2329547904b97abc52fd5e76b008d8 to your computer and use it in GitHub Desktop.
Undo/redo history pattern example in JavaScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const undos = []; | |
| const redos = []; | |
| // usage example: undoable(()=> { state.width = 100; }); | |
| // (this also supports calling undoable with no action function, before | |
| // continuously changing the state (e.g. adding points to a brush stroke), | |
| // but this is not recommended; it would be better to, for instance, | |
| // have a separate state for a brush stroke in progress, | |
| // and add it to the main state as an undoable step when it's finished; | |
| // this would also let you cancel the operation simply) | |
| const undoable = (synchronousAction) => { | |
| // you may also want to mark the document as unsaved here | |
| // TODO: don't destroy history as is the common practice, but instead provide nonlinear history - | |
| // keep a tree of states and show this tree to the user when they attempt to redo after undoing and doing something other than undo/redo; | |
| // that way they can at least get back to any state, if not merge states after diverging, which could be complicated; | |
| // also provide a way to show the tree at any time, perhaps ctrl+shift+y | |
| redos.length = 0; | |
| undos.push(getState()); | |
| if (synchronousAction) { | |
| synchronousAction(); | |
| onStateChanged(); | |
| } | |
| return true; | |
| }; | |
| const undo = () => { | |
| if (undos.length < 1) { return false; } | |
| redos.push(getState()); | |
| setState(undos.pop()); | |
| return true; | |
| }; | |
| const redo = () => { | |
| if (redos.length < 1) { return false; } | |
| undos.push(getState()); | |
| setState(redos.pop()); | |
| return true; | |
| }; | |
| // --- now interface with the application --- | |
| let state = { | |
| // if you're going to save files based on the serialized state OR persist it in localStorage, | |
| // you should include a version number, so old states can be upgraded, | |
| // and the user can be told when old states aren't supported | |
| // for an example of this pattern, see https://github.com/1j01/mopaint/blob/2db35857fbbb76980ce1229dadef531bf5147fc6/src/components/App.js#L60 | |
| // (that one TODO comment is accidentally left in there, it's done with `nounPhraseThingToLoad`) | |
| // formatVersion: 1, | |
| width: 640, | |
| height: 480, | |
| strokes: [] | |
| }; | |
| const onStateChanged = () => { | |
| // could update any computed (derived) state here to ensure consistency when saving | |
| // or better yet, keep computed state out of `state`! | |
| // (derived state would be, for instance, if your app is a traffic simulator, and the user places roads, | |
| // but buildings and trees are filled in automatically as decoration, | |
| // if the buildings and trees are a function of the road structure, they are derived state) | |
| // you should probably save the user's data: | |
| /* | |
| try { | |
| localStorage.appState = getState(); | |
| } catch (error) { | |
| // show a warning here that data isn't saved | |
| // but don't use alert() or anything that obtrusive | |
| // (the user should still be able to use the tool with site data turned off in their browser, | |
| // but just be made aware that their data isn't safe (and why)) | |
| } | |
| */ | |
| render(); | |
| }; | |
| // serialize the state | |
| const getState = () => JSON.stringify(state); | |
| // deserialize and apply a state | |
| const setState = (stateJSON) => { | |
| state = JSON.parse(stateJSON); | |
| onStateChanged(); | |
| }; | |
| const render = () => { | |
| // TODO (render based on state) | |
| }; | |
| /* | |
| // load from localStorage | |
| try { | |
| if (localStorage.appState) { | |
| setState(localStorage.appState); | |
| } | |
| } catch(err) { } | |
| */ | |
| render(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment