Programmatic changes in tlDraw without affecting undo/redo stack
While working with the tlDraw framework for infinite canvases, you may want to make programmatic changes to the content in the editor without the user being aware of them. That means that some of your modifications shouldn’t appear in the undo stack.
If you let your programmatic changes register on the undo stack, often, each user initiated action could result in multiple programmatic changes behind the scenes. Meaning if the user tries to undo their action, they have to undo several other changes as well. Changes that they didn’t event realise occurred.
This article looks at handling that.
An example problem
Imagine you create a flowchart app that allows the user to drag flowchart bubbles around. When they drag a bubble close to another bubble, the app automatically connects them with a line.
The problem
In this example, the user might drag a bubble (One user action), but upon releasing their mouse, the app automatically draws a line between the bubbles, adds a label to the line, and changes the colour of the bubbles (Three programmatic changes).
By default, this creates four undo entries. one for the bubble colour changes, one for the label, one for the line, and one for the bubble drag. But as far as the user is aware, they only did one action, so if they choose to undo, they expect to only have to undo once.
Problem code
If you do the below, it will add an entry onto the undo stack for the colour changes.
editor.updateShapes([
{
id: shapeId1,
type: 'bubble',
props: {
color: 'green'
}
},
{
id: shapeId2,
type: 'bubble',
props: {
color: 'green'
}
}
]);
The solution
There are a couple of ways to handle this and your approach should change depending on how your program works.
Squash or discard
One way to handle this would be to squash all of the changes into one undo entry. This means that when the user selects undo once, tlDraw will undo the colour change, undo the label, undo the line, and undo the bubble drag as if they were all one action. This may be the option you want, but not necessarily in this case.
In our instance, we have likely programmed the app to programmatically reassess lines, labels, and colours whenever anything changes, so we don’t actually need to undo those. Instead, we undo the user’s bubble drag, and everything else will recalculate automatically – But bare in mind, your app needs to be built to work like this (ie. after every tldraw change, it recalculates and adds/removes what’s needed).
Essentially, in this example, we can tell tlDraw’s undo stack to ignore any of the changes we did programmatically. To do this, when we make the changes, we pass a flag to the method to indicate it is ephemeral.
Solution code
editor.updateShapes([
{
id: shapeId1,
type: 'bubble',
props: {
color: 'green'
}
},
{
id: shapeId2,
type: 'bubble',
props: {
color: 'green'
}
}
], {
ephemeral: true,
});
That’s it!
For the official documentation on this, refer to the tlDraw documentation for TLCommandHistoryOptions (Which is the object we’ve passed in above), and the editor.updateShapes method it’s passed into.
There are also two other history options we haven’t directly address here (squashing, and preservesRedoStack). But let’s leave them for another article.
Thanks…
I also dissect and speculate on design and development.
Digging into subtle details and implications, and exploring broad perspectives and potential paradigm shifts.
Check out my conceptual articles on Substack or find my latest below.
You can also find me on Threads, Bluesky, Mastodon, or X for more diverse posts about ongoing projects.
Leave a Reply