Now let's go ahead and let's implement the history functionality including the undo and redo. So I want to go inside of my use editor hook right here. And inside of the use editor we're gonna go ahead and introduce a new hook which we are going to call useHistory. So let's go directly inside of the useEditor like this and let's go ahead and let's add above here a new useHistory hook. Now let's go ahead and let's create that hook.
So use-history.ts And now inside of here I'm simply going to return this hook and let's go ahead and let's create a save function which will have a use callback just like this and let's go ahead and for now just add a console.log saving and let's return the save callback Now that we have that we can go back inside of the use editor and we can import the use history. I'm gonna go ahead and change the import. There we go. And now let's go ahead and let's actually extract Save from here. And I want to pass this Save to CanvasEvents right here.
So I'm gonna give it the save method inside. And inside of this useCanvasEvents let's now add the save function here. Like that. And then we can destructure it here in the props as well. So useCanvasEvents is in the same folder as use editor, use clipboard, our new use history and all other hooks we have.
And now we're going to go ahead and simply add some more events here. So let's add canvas on and we're going to add first of all object added so if object is added we call the save method then if object is removed we also call the save method and if object is modified we call the save method And then we also have to add canvas off for all of those. So object added, object removed, and object modified. Like this. And we have to give save inside of the dependency array right here.
Great! So we now have that and I think we're ready to try it out. So instead of use history, this should now fire every time a new object is modified, added or removed to our canvas. So I'm gonna go ahead and open this and I believe I logged saving so that's what I'm gonna search here. Saving, right?
So let me go ahead and clear everything and let's add a shape. There we go. You can see it says saving here. Let me move it. It says saving.
If I rotate, saving again. And finally, if I delete it, saving. Perfect. So we now know that our connection between the hooks is working properly and what we are going to do now is we are going to actually develop the history index and the actual history state. So let's first of all add the history index and set history index.
So this will be used to render specific states like whether we can, whether our undo or and redo buttons are going to be disabled or not. So we're going to start by history index being zero, right? Meaning the first item in the array. And then we're going to define canvas history, but we are going to store this inside of ref instead of state, because we are not going to use this to render things directly. And by default it will be an empty array.
Otherwise it's going to be an array of strings, which is basically going to be JSON stringified history of our canvas. Let's go ahead and also add skip save which will be a useRef with false by default. Skip save will be used so that we can prevent the save method being triggered when we undo and redo because when we undo and redo things this will still fire so object added will fire again on undo and redo so it doesn't make sense that that gets triggered again it will just fill the history infinitely so we have to find a way to prevent that from happening looks like I have some chunk errors here what I'm gonna do is just refresh I think it's just some cache, nothing we've written here great, so we have skip save now and what I want to do now is define const and undo so that's gonna be a use callback so we're gonna write everything inside of use callback here because we're gonna use these things in dependency arrays right so very simply, we're going to return if historyIndex is larger than 0, that means that we can undo. And let's simply add historyIndex inside of here.
And then let's write const canRedo that will be useCallback again which will check if historyIndex is less than canvasHistory.current.length minus one like that and pass in the history index and we don't have to pass in the canvas history because canvas history is a ref and not a state. Great! So now we have the methods which can disable or enable the undo and redo buttons in our toolbar or navbar here above. Great. And now let's actually implement the save method here.
So let's go inside of save here. And first things first, we have to create proper use history interface. So interface use history props. We'll accept the canvas, which will be a type of fabric dot canvas or now, because we need the canvas state so that we can store it. And I just feel safer if we have fabric from fabric actually added here.
Besides that, well, let's just work with this for now. Later we're gonna add some more interesting stuff. So use history props here and then we can destructure the canvas. Great. So inside of save here, first things first, we're gonna check whether we have the canvas or not.
So if we don't have canvas, we can just break this method. And let's add canvas inside of the dev dependency array and you can actually collapse this because we're gonna have a lot of stuff inside. Great! Now let's go ahead and let's get the current state using canvas to JSON like this and then JSON will be JSON stringify current state like this But what I want to do is I want to create some specific values which we're going to put inside of this toJSON method. So the toJson method accepts the properties to include params Which means any properties that you might want to additionally include in the output.
So by default there's a lot of There's a lot of things like for example selectable has controls which won't be included in this toJSON function. So we're gonna save our state to JSON but then this has controls and selectables which we have in our initial workspace which are pretty crucial to us won't get saved. So we have to manually tell inside of here that they will be saved. So I want to go inside of the types in the editor. I'm going to go all the way to the top and I'm going to export const JSON keys and I'm going to write everything that I want to save here so name gradientAngle selectable taskControls linkData editable extensionType and extension Like this.
So those are all the keys I always want to save. So let's go ahead inside of here and just add the JSON keys from slash types or features, editor types. There we go. So now we have the proper current state inside of here. So what I wanna do now is I wanna confirm that we aren't supposed to, you know, skip this state right here.
So let's go ahead and do the following if not skip dot current my apologies skip save which we define right here so if this is not enabled then we're gonna go ahead and push to canvas history.current so we're going to push here the new JSON which we've just stringified like this and we're going to change the history index to be the current canvas history .current.length minus one basically to the last one in the array. Like that. And then inside of here, we're going to do a to do saveCallback like this. Basically we don't have the save callback yet but this is where we are going to call our actual save method which will save to the database using autosave in real time. So we're going to reuse this history hook since we already have this save feature here for local saving.
We are going to reuse it with a debounced method which will also auto save to the database later when we actually have the database and I just want to further advance this save method. I'm going to give it a prop here which will be called skip and by default it will be false. So I want two ways to prevent this save method from writing to the history ref. The first one is using the global skip save ref, right? That's the first one.
So this skip save prevents our use canvas events from pushing to history when they don't have to push to history but this one save with the skip inside of here will be used when we want to directly call the save method but we don't want to add that to undo or redo. So that's why I want to add this as well. So perhaps you can think of some better names maybe global skip and local skip something like that I don't know so we're just going to extend this as well so if not skip and if not skip save so I don't want this to be prevented by the global skip nor the normal skip. Right? So just make sure that you do it exactly like this.
Right? Don't do an early return like if skip or if skip save current return. Don't do this because we need to reach this part as well. So remove this and make sure that you write it like this. Great.
And now let's go ahead and let's actually implement the undo and the redo methods. So const undo is use callback. Like this. And redo will be pretty much the same so let's go ahead and go inside first things first we're gonna go ahead and wrap this inside of can undo like this so we can immediately add can undo inside of the dev dependencies. My apologies, in the dependency array.
So first things first, while we are undoing something, we are gonna enable the global skip save to be true, which means that whatever we do in this undo method if it accidentally triggers anything inside of here it will not do this part meaning that it won't push to history because we don't need it to push to history but it will call the database saving method which is something that we do want if we undo we want to save that to the database we undued we are now in the previous version great so that's the first thing we have to do and then we're gonna go ahead and call canvas clear and render all So let's go ahead and also add canvas to the dependency array here. Let's go ahead and let's get the previous index, which will be history index minus one. We now know that we also have to add the history index inside of here. Besides the previous index, we're also going to have the previous state. So that will be json.parse canvas history.current previous index.
Like that. And then, now that we have the previous state, we're gonna do canvas.loadFromJson and we're simply going to load what? Well, the previous state. That's what we're going to load. We're going to load the previous state and now the second argument needs to be the callback.
So what happens after we load the state? So first things first, we're going to call the renderAll() method and then we're going to call setHistoryIndex to the new previous index like this and then we can enable skipSave.current well I mean disable the global skipSave like that there we go and now let's go ahead and copy the entire thing and let's paste it inside of redo here because it's gonna be quite similar so the dependency array will need the same things canvas, history, index and can redo What else am I missing here? Canvas history index. Oh yes, so we will use canReview here of course. And we enable the skip save, we clear all.
And now instead of calling the previous index we are getting the next index so the function here will be history index plus one instead of previous state we will have the next state meaning that it's going to call canvas history dot current next index and that's what we are going to parse and then we are going to load from JSON and not the previous state but instead we are going to load the next state and we are going to assign the history index to the next index like this. There we go! So we just implemented our own history management here and now besides save let's also go ahead and return save can undo can redo undo redo set history index and canvas history Like that. There we go. So now let's go ahead and let's go inside of use editor here.
And first of all, we have a little bug that we have to fix. It's right here in the use history. So we have to pass in the canvas. There we go. And now besides save we're gonna have canRedo, canUndo, undo and redo.
All of those methods. We also have some more things which use history's returning such as setHistoryIndex and canvasHistory but we're not gonna need that right now. We're gonna need it later. But for now, these are the methods which we want to pass to our build editor. So let's go ahead and pass in the save, the undo, the redo, canUndo and canRedo.
I believe those are all the things which are returned from here. There we go. So now we have to go inside of the build editor props type and let's go ahead and add the undo, redo, save, can undo which will return a boolean and can redo which returns a boolean as well. There it goes and once you save this your use editor should no longer have any errors. But we still have to remember to add all of those things to the editor use memo dependency array.
So let's add the can redo, can undo, undo, redo and save. There we go. And now we can go ahead inside of the actual build editor extract all of those new things and can undo and now we can go ahead and well play with that so first of all inside of here we already have this comment to do save so you can see that this is where what's gonna come useful is the save With the false option that will come in useful in a moment And also yeah, this reminds me that I gave our save method the wrong type here. So save also has the skip Which is a boolean and it can be optional like this so when we want to call save like in change size or change background because this doesn't trigger any canvas event so we manually want to call this but we want to skip this going inside of the history for example this is where we're going to use that but not yet because we don't have the database added. I'm immediately going to contradict myself so I just pause the video and I thought to myself what I just said but it makes no sense that we don't add it here right.
The only thing we need here is for change size and change background to be part of the history right. We need to undo and redo even if we did things like that. So let's go ahead and do that. Let's add the save and we don't even need to pass in the skip right so we are just gonna call the save which we now added right here in the build editor there we go we are now using it here perfect so what I also want to add now is the following. I want to add the same way we added our onCopy and onPaste.
Let's also add onUndo to call our undo and onRedo to call our onRedo, sorry, redo methods. But we also now have the can undo and can redo. So I want to go ahead and copy those in. I don't know, let's put them here, for example. I basically want to separate those with which I just, you know, don't even rename and these other ones which I rename at least in the slightest.
You of course don't have to. If you want to return this as undo and redo, copy and paste, sure, you can. I just kind of want to differentiate between them great and now let's go ahead and go inside of the editor interface and we have to return all of those again this time so we're going to have the undo which is a simple void it's going to be on undo then on redo And can undo which will return a boolean and can redo which returns a boolean as well. And I think that is all. There we go.
Yes, use editor now seems to be working just fine. What we have to do now is we have to go back inside of our main editor and inside of here looks like we have a navbar, right? So what we have to do is we have to, well, we have to now add the editor inside of the navbar. So let's go ahead and pass in the editor to be our editor like this. Let's go inside of the navbar, let's add editor be a type of editor or undefined.
Make sure you've added the editor import from features editor types alongside the active tool right here. And now inside of here, we're going to be able to do some fun stuff so let's go ahead and let's find our undo and redo buttons here so not this, not the opening of the JSON file, not the select tool but here we go the undo and the redo so we're gonna do the following we're gonna disable this button if !editor.canUndo so if we can't undo oh we also have to destructure the editor here there we go. So if we can't undo we're gonna disable this button right here and we don't need any dynamic class here. And inside of here we can just call editor on undo like this and we can remove the comment now and I believe it's the same thing here so just copy and paste the disabled here call the can redo here and inside of here call editor question mark on a redo. You don't need this class name I'm pretty sure.
I don't remember why I added that and we don't need this comment as well. And now there we go. So you can see how both of my buttons are now disabled. So let's go ahead and try it out now. I'm going to add a circle.
Looks like it's not... There we go. So it works, but not immediately as I've noticed. So if I go ahead and start clicking this, there we go. So you can see that we have somewhat of a history, but you can see something's wrong, right?
So we can't go all the way to the end and the history wasn't immediately available right so right now it's disabled I click on this and it's still disabled only after another move is when it becomes active right And the redo also works just fine. But we have this little bug, right? Where we can't go all the way to before we've added the object. And the reason for that is because we did not initialize the history in the use editor. So let's go ahead and do the following.
Let's go inside of source, features, editor, hooks, use editor here and I actually want to go directly inside of the actual use editor hook this time and I want to let's see whether I'm gonna do that here or maybe we should create another hook for that so let me just revisit my init method so perhaps this could be the place where we also initialize our history for the first time so it holds that empty state as the first item in the history array. So for that we're actually going to need those things that I told you we're going to need later. So from the use history hook here besides save, can redo, undo and redo we are also going to need the actual canvas history and the setHistory index. So we're gonna need these two. And now let's go inside of the init method here and after setCanvas and setContainer we're gonna get the current state from the what we just initialized so we're gonna write json.stringify() and we're gonna pass in this initial canvas which we've just created here and we're gonna use that as the initial item in the history array.
So initial canvas, my apologies. Yes, initial canvas to JSON. And we're gonna pass in JSON keys. Like this. So just go ahead and make sure you've added the import for JSON keys from features editor types like this.
I think that's okay and then inside of canvas history.current go ahead and add the current state and set history index to zero like this and here's the thing so now it's warning us that we have to add canvasHistory inside of here and setHistoryIndex. What we know, but our linter does not know is that the canvasHistory is a ref and setHistory is a dispatch function. So technically, I'm gonna add a comment here no need, this is from let's say useState and I'm gonna change this to useRef So I just want you to write a comment so that you know that you technically don't need this in the dependency array and nothing will change if you remove this from the dependency array but the linter does not know that because all that linter sees here is that we got those methods from here, right? So to the linter, these are just some weird methods. It has no idea how it got them, even if we have the correct types.
So this is obviously a dispatch and this is a ref object. Linter does not know that, right? So if you go inside of here, you can see that it is a ref. It's not something we add in dev dependencies, right? We don't have to add that here.
And same is true for the set history index. We also don't add that to dependency arrays. But we do have to add it here just to get rid of the errors right here. But the good thing is our init method will not be reinitialized any more than it was just a moment ago. Great!
So let's save this and now let's refresh our page. So what happened now is that this was initially saved as the first history and now when I add a shape there we go you can see how my undo is immediately available and I can even redo back. Amazing! So now we have fully functioning histories here and now you've kind of learned how to load and save things from JSON and to JSON. We've kind of explored that now.
Here we save to JSON and then when we redo and undo we load from json so that's what we're gonna reuse now for these export and open or upload features now nevertheless great great job Thank you.