undo redo functionality in Angular app
Asked Answered
B

1

12

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?

Braiding answered 30/3, 2018 at 8:15 Comment(1)
I had this situation and I preferred ExecCommands from Mozilla developer.mozilla.org/en-US/docs/Web/API/Document/execCommandUngraceful
V
3

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

Viewable answered 8/9, 2019 at 7:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.