Implementing Undo/Redo can be tricky business. Consider two strategies to simplify implenting undo/redo: 1) use immutable data 2) react to that data with a top-down approach. Let's build a simple drawing app that will draw circles on a canvas and allow us to undo/redo our drawing.
Implementing undo/redo can be tricky business. Consider two strategies for implementing undo/redo. One, use immutable data. Two, react to that data with a top-down approach. Let's build the simple drawing app that will draw circles on a canvas, and allow us to undo/redo our drawing.
One, managing our immutable store, and two, updating our component if that store changes. I've instantiated this model, which will be used for this drawing canvas, which I've implemented is a react component.
The react component itself has a few simple methods, a handle undo and a handle redo, which will use that model, and a handle art board click event that will take the XY coordinates of a mouse click on this canvas here, and add it as a point to the model.
The render method is pretty simple. It's basically an SVG art board, just an SVG rectangle that will have circles added to it, and two buttons that will handle the undo/redo.
I've also created this function down here called "RenderComponent," which will render the drawing canvas with any properties you want to pass into it, and our model will actually pass the history and history index to it.
To kick things off, I called the RenderComponent to get this on the screen. To start here, we need actually create two things, the history index and a history. The history is going to be a list. It's going to be a list, which is going to contain a stack.
The history index here will be used to point at, within the component, the component itself will use the history index to point at one of the stacks within the list, which will represent a period in time in the undo/redo.
Each point in history, each action will generate a series of points. The series of points will also be stored as an immutable list, as well, and we'll just make an empty one for now, because the first action is always going to be nothing, because we haven't done anything just yet.
In the update method, we want to make sure that the model renders our components. We'll write RenderComponent, and this is actually key to everything here. Our model is responsible for rendering our data with a top-down approach, meaning that the react component itself will never mutate data.
It relies on some other structural entity do so, and it will only react to that data. Now, we'll have the model pass in the points, which are generated by the history, and where we are within that history. We'll pass in the history index.
Perform operation is the key method here. Basically, it will take and create a new history for every action. Let's go ahead and grab our history, the current state of it, zero to the history index plus one.
Then, we're going to create a new version of that history that will pop on the end of the stack. We'll say, let new version equals function, this.history.getthis.historyindex.
What we're doing is we're taking this function, which is our action, and then that action we're going to create some data. We give it the current stack state.
This is the current state of the history, passed into that function, and the function will return back a new version of that state, which we will then push on to the history, we have another moment in time that we can reference version.
Then, we'll take the history index and increment it one time, since we have a new state. We'll call this.update, which will then update and allow the component to re-render. Let's see how we add a point. We need to call this.perform operation, and we'll pass in a function that will return back our new state.
The new states going to look like this, data.push, and we're going to hold our points inside of an immutable map. Map with a few keys. The keys are going to be point.X, point.Y, because X and Y are the keys, and ID of a date string.
To undo is pretty simple. All we need to do is manage that index, and tell the component at which point in time do you want to render. Let's check the history index to see if it's greater than zero, and then, we can decrement it and update our component.
For redo, it's similar, but in reverse. This.HistoryIndex is less than this.history.size, and then, we can increment history index and call this.update. That is all the model needs to be. It just has a point in time that needs to tell the component to render.
The component itself needs to take a look at that data. Remember, it's getting two properties passed into it, points and history index. Let's go ahead and parse out that data. Let's make sure it's their first. We'll do if this.props.points, and this.props.HistoryIndex. We have both of them, and we should be able to do something with them.
I want to take this.props.points.get, we're going to get our current place in the history stack. This.props.HistoryIndex, and then, for those points that we have at that moment in time, we're going to loop through them for each. We're going to take a method, take each point and create a circle for it.
Create circles.push, and we'll create an SVG circle with a key of point.getID, a radius of five, a center X of point.getX and a center point for Y the same way. Then, we'll give it a fill color of white. One, two, three, four, five, six. We'll cap it off, close it off, and then, we will then take care of their redo disable and undo disable.
What these do is they're going to look at the history index and say, "Can I go backwards in time? If I can, cool, enable the button. Can I go forward? If I can, cool, enable the button." Undo disabled will look like this. This.props.HistoryIndex is equal to zero.
If it's equal to zero, it'll be disabled, and then redo disabled will look like this.props.HistoryIndex is equal to this.props.points.size. If we're at the end of the stack, we can't go forward in time anymore, and that's all there is to it. That's how we get our component to work.
Let's go ahead and take a look and see if this works. If we click, we should see a point. You can see, I'm making all of these points here, and if you click redo, you know what? I need to make that index up top. Let's re-render that and start this over.
We should be able to click, and then when we undo, we should see them go back in time, and then redo, forward in time. That's how you implement a very simple undo/redo.