I am thinking of ways to implement undo,redo functionality in an angular app.
The first and most basic idea was to use a model that completely describes the app internals, lets call it AppModel
. On every change worth recording, you simply create a new object of AppModel
and push it on to the stack and update currentIndex.
In absolute worst case scenario, an object of AppModel would have to fill out 500 text fields, with average length of 20 characters. Thats 10000 characters or 10kB for every update.
How bad is this number? I don't think if would cause memory issues, but would it make the app freeze every time a push onto stack happens? This a basic implementation:
historyStack: AppModel[];
currentIndex:number = -1;
push(newAppModel:AppModel){
//delete all after current index
this.historyStack.splice(++this.currentIndex, 0, newAppModel);
}
forward(){
if(this.currentIndex < this.historyStack.length-1){
this.currentIndex++;
return this.historyStack[this.currentIndex];
}
return this.historyStack[this.currentIndex];
}
back(){
return this.historyStack[this.currentIndex--];
}
The other option I could think of, is to store function calls that perform the redo
and reverse
operations. This approach would require me to also store which objects need to call the functions. Those objects might me deleted by user, so there also must be a way to recreate the objects. This is getting painful as I type :)
What way do you recommend?
To undo an action press Ctrl+Z. If you prefer your mouse, click Undo on the Quick Access Toolbar. You can press Undo (or CTRL+Z) repeatedly if you want to undo multiple steps. Note: For more information about the Quick Access Toolbar, see Customize the Quick Access Toolbar.
To undo an action, press Ctrl + Z. To redo an undone action, press Ctrl + Y. The Undo and Redo features let you remove or repeat single or multiple typing actions, but all actions must be undone or redone in the order you did or undid them – you can't skip actions.
If “UNDO” string is encountered, pop the top element from Undo stack and push it to Redo stack. If “REDO” string is encountered, pop the top element of Redo stack and push it into the Undo stack. If “READ” string is encountered, print all the elements of the Undo stack in reverse order.
A multi-level undo system increases the number of activities that may be reversed. Each important action is recorded internally and the user may invoke the undo function several times to cycle backwards through previous states. Multi-level undo is often paired with a redo command.
That's why it's recommended to have the state not in one single object, but to work with (business) modules where each module has a reasonable amount of properties.
I would recommend using a Redux framework like NGRX or NGXS for state management. For NGRX, there is a meta-reducer-library https://www.npmjs.com/package/ngrx-wieder which can wrap your NGRX reducers like this:
const reducer = (state, action: Actions, listener?: PatchListener) =>
produce(state, next => {
switch (action.type) {
case addTodo.type:
next.todos.push({id: id(), text: action.text, checked: false})
return
case toggleTodo.type:
const todo = next.todos.find(t => t.id === action.id)
todo.checked = !todo.checked
return
case removeTodo.type:
next.todos.splice(next.todos.findIndex(t => t.id === action.id), 1)
return
case changeMood.type:
next.mood = action.mood
return
default:
return
}
}, listener)
const undoableReducer = undoRedo({
track: true,
mergeActionTypes: [
changeMood.type
]
})(reducer)
export function appReducer(state = App.initial, action: Actions) {
return undoableReducer(state, action)
}
This way you don't have to write the undo/redo logic for each module's reducer over and over again, just wrap it in the meta reducer instead. And you can exclude the heavy part of the state that does not need to be undone. You can find a full Stackblitz example, as well as the basic part of the implementation code (utilizing patching with ImmerJS) here: https://nils-mehlhorn.de/posts/angular-undo-redo-ngrx-redux
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With