projectstorm / react-diagrams

a super simple, no-nonsense diagramming library written in react that just works
https://projectstorm.cloud/react-diagrams
MIT License
8.58k stars 1.17k forks source link

Keep track if canvas has changes to conditionally render a save button? #819

Open nabramow opened 3 years ago

nabramow commented 3 years ago

I'd like to conditionally render a Save button at the bottom of the screen only once there have been changes to the canvas, i.e. (nodes moved/added/deleted, links changed etc.).

Sounds simple enough in theory, but I'm having a lot of trouble finding one place to check for changes without writing a lot of complicated logic.

I've tried checking for when a 'nodesUpdated' or 'linksUpdated' event fires and dispatching an action to signal changes to the canvas then, but that's causing some weird behavior with the canvas.

Normally I'd just save something in the local state and compare. I can see that there's a StateMachine listener in the StateMachine file, but not quite sure how to utilize this. I can use this.engine.getStateMachine().getCurrentState() to get my current State, but how to I find/compare it with my previousState?

Has anyone done something similar? I feel like I must be missing something obvious!

renato-bohler commented 3 years ago

Hey!

Hmm, there's two approaches I can think of.

  1. Very simple, but expensive: serialize the diagram whenever it is saved and store that on a lastSaved state as a string (JSON.stringify(model.serialize)). Do the same thing every X seconds, but instead of saving on a state, compare it to the last state. This is expensive because serializing the whole diagram is not a cheap operation... and has the downside of the delay that will happen between the user doing something and the save button enabling.

  2. More complex, but efficient: start with the state changesSinceLastSave with value false. Listen to a list of events that you want to consider "changes", and whenever one of them happens, just set changesSinceLastSave to true. Whenever the user saves the diagram, set this to false again. This is more complex, because as far as I know there's no proper documentation on which events are emitted and when... so my suggestion would be to change the fireEvent on the BaseObserver class to log every event that is fired and see if an event is fired for every case you need (you can do it directly in your node_modules just to analyze). If there's any action that does not fire any event, then you would probably need to implement your custom state and fire them manually. There's an example on how to implement your custom state on the demos. On my project, I've been using something similar to this to implement undo/redo. For instance:

Using that strategy didn't cause any weird behavior in my case. It has been working fine.

danielmcquillen commented 2 years ago

@renato-bohler Thanks for posting this!

Following it to good effect, but any suggestions on how to track position changes? I thought I could add a positionChanged to the model like below...

  this.model.registerListener({
            linksUpdated: (e: any) => {
                console.log("linksUpdated ", e);
                this.setState({changesSinceLastSave: true});
            },
            nodesUpdated: (e: any) => {
                console.log("nodesUpdated ", e);
                this.setState({changesSinceLastSave: true});
            },
            positionChanged: (e: any) => {
                console.log("positionChanged !", e);
                this.setState({changesSinceLastSave: true});
            },
        });

... but realizing now that unfortunately a global positionChanged event isn't broadcast from the DiagramModel if any node is moved.

Any thoughts on how to watch for this across any node in any layer?

Just like the original poster, I'm trying to come up with the simplest way to know when to enable the save button.

renato-bohler commented 2 years ago

@danielmcquillen I'd say you need to create a custom state, just like I mentioned before, but adding a custom implementation for MoveItemsState, which will fire an event with a name like entitiesMoved when triggered.

You can then listen to entitiesMoved instead of positionChanged.