nilsmehlhorn / ngrx-wieder

Lightweight undo-redo for Angular with NgRx & immer.js
https://nils-mehlhorn.de/posts/angular-undo-redo-ngrx-redux
MIT License
126 stars 11 forks source link

Feature Request / Question - Possibility to see differences of Undo / Redo within an Effect #20

Closed wipedx closed 3 years ago

wipedx commented 3 years ago

Hi Nils,

I'm fairly new to the whole concept of ngrx, immer.js and wieder.js so sorry in advance if this does not really concern wieder.js and can be solved otherwise. I'd like to access the changes reversed or re-reapplied in an ngrx effect in order to reflect the undone/redone changes within a database. Reapplying the whole context would be possible, but a huge performance impact when there's only one object that needs to be adjusted.

Is there a possiblity that the Undo/Redo actions can put out the applied changes to an effect or that these differences can be accessed somehow?

nilsmehlhorn commented 3 years ago

Hey @raohag !

Just to clear up the naming. There's the library NgRx, then there's immer.js and this library is just called ngrx-wieder as some kind of wordplay on immer.js

I've been thinking about something similar where I'd forward the patches to a server in order to enable realtime collaboration. I'd say it's definitely possible. The patches are generated by immer.js in a format similar to the JSON Patch standard:

I think instead of having an effect, it'd be easier to just store patches in the store - similar to how the canUndo / canRedo flags are stored. Would that work for you? Then you could just subscribe to the undo & redo stacks with their respective patches.

wipedx commented 3 years ago

Thanks for the quick and thorough response! I'll have a look into it, but in theory this should work.

alexfarrugia commented 3 years ago

hi @nilsmehlhorn , this is a real cool and handy library in my opinion and thus make undo and redo management much more lightweight than if one had to store state slices.

I would also be very interested in such a feature, particularly to be able to map the undos or otherwise to my database as @raohag mentioned. However i would also be interested in looking into the array containing such patches in the store to be able to understand how many undos/ redos are available... This would allow me to enable or disable the undo function accordingly.

nilsmehlhorn commented 3 years ago

Hey @alexfarrugia, thanks for the feedback. As I said, we could put the history information (i.a. the patches from immer) into the store. Maybe the lib could/should even provide some selectors for the number of available undo/redo steps - would allow us to drop canUndo/canRedo and thus simplify some code.

I'll only have time for doing this maybe next month. Happy to review a PR and provide some guidance though.

alexfarrugia commented 3 years ago

Dear @nilsmehlhorn , i have to admit that i don't feel confident enough to submit a PR myself, i am working on my first angular project and have only been working with Angular for the past 4/5 months. While i can say that i am really enjoying learning it and getting more comfy with it, I still feel that there is quite a bit for me to learn so I would prefer waiting for someone who is really more knowledgable and experienced like yourself with something like this :).

julianpoemp commented 3 years ago

I'm new to immer.js and ngrx-wieder and try to implement undo&redo in my application. Before I can implement it I try to figure out how to save undone/redone actions to my database (IndexedDB). I did some experiments on the ngrx-wieder library on StackBlitz.

The problem is, that I need a way to save the changes to the database after each undo/redo action, but I don't know what kind of action was undone/redone in order to call the specific methods for saving. What I need is, that the undo/redo action gives me information about the redone/undone action. With that, I could build an effect with if statements that call the correct saving methods.

Im thinking about something like that (just an example, not working):

app.actions.ts

...
export const doUndo = createAction("UNDO", props<actionType: Action>());
export const doRedo = createAction("REDO", props<actionType: Action>());
...

app.effects.ts

import { Injectable } from "@angular/core";
import * as AppActions from "./app.actions";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { Subject } from "rxjs";
import { Action } from "@ngrx/store";
import { exhaustMap } from "rxjs/operators";

@Injectable({
  providedIn: "root"
})
export class AppEffects {
  checkAdd$ = createEffect(() =>
    this.actions$.pipe(
      ofType(AppActions.doUndo),
      exhaustMap(action => {
        if(action.actionType instance of AppActions.addTodo)
        {
             // save that part that was affected by addTodo to the database
        }
      })
    )
  );

  constructor(private actions$: Actions) {}
}

Is there any better/easier way to solve the problem of saving changes after redo/undo to the databse?

The only other thing that is on my mind is saving the whole state after each redo/undo to the database. It's not the best way, but probably the easiest. I created a test project of this approach: https://stackblitz.com/edit/ngrx-wieder-effects-easy-version

I think instead of having an effect, it'd be easier to just store patches in the store - similar to how the canUndo / canRedo flags are stored. Would that work for you? Then you could just subscribe to the undo & redo stacks with their respective patches.

@nilsmehlhorn I don't know how to implement this. Can you give us an example, please?

nilsmehlhorn commented 3 years ago

Hey @julianpoemp, again I think this might be solvable if the undo-redo information would be stored in the state. Also, currently the information regarding which patches belong to which action is lost. The library would need at least this extension (like UndoRedoStep below), currently only the patches are kept, here:

https://github.com/nilsmehlhorn/ngrx-wieder/blob/98f9d0da38c98d431bc2b4fda129223935dcd803/projects/ngrx-wieder/src/lib/undo-redo.reducer.ts#L108

// model representing the change of one NgRx action
interface Step {
  patches: Patches
  action: Action
}
// history state which you'll extend with your reducer state
interface UndoRedoState {
  histories: {
    // index type to facilitate segmentation
    [id: in string | number]: {
      undoable: Step[]
      undone: Step[]
      mergeBroken: boolean
    }
  }
}

I'm kinda thinking there might be problems in regard to merging steps of different action types, I'll need to see

export const doUndo = createAction("UNDO", props()); export const doRedo = createAction("REDO", props());

The problem with these action creators is, that you don't necessarily know which action you're undoing when dispatching the undo/redo actions. That's currently decided by the meta-reducer and I think it's best to keep it that way - anything else would be error-prone. However, when the history is accessible from the state you can retrieve this information after undo/redo was performed like this:

undoRedo$ = createEffect(() => this.actions$.pipe(
  ofType("UNDO", "REDO"),
  withLatestFrom(this.store.select(state => histories["DEFAULT"]),
  map((action, history) => {
    if (action.type === "UNDO") {
      const [undoneStep] = history.undone
      console.log(undoneStep.action.type) // type of action that was undone
      console.log(undoneStep.patches) // applied patches
    } else if (action.type === "REDO") {
      const [redoneStep] = history.undoable
      console.log(undoneStep.action.type) // type of action that was redone
      console.log(undoneStep.patches) // applied patches
    }
  })
))

Would that work? Maybe we should consider not storing the action payload - maybe in an optional manner where you'd configure the types for which you need it.

nilsmehlhorn commented 3 years ago

I've implemented a first version in #29

@julianpoemp @raohag @alexfarrugia the implementation would give you access to the patches from the state just like I've outlined in my previous comment you can get the patches and action types from the last step in an effect and the do what ever you want with them, e.g. forward to a server.

I'd appreciate any review, maybe you can even check your use-case by building the PR branch against an application.

Furthermore, I've listed a few open considerations in the PR and would appreciate any input on those. 🙂