by Maurice de Laat
Designing an extensible undo/redo history using TypeScript and the Command Pattern.
In desktop applications, the ability to undo or redo a user’s latest action has already become ubiquitous many years ago. So it’s not surprising that, with web applications replacing their desktop counterparts more and more, users are just expecting it from modern web applications as well. The good news is that implementing an undo history doesn’t have to be hard. However, implementing it right from the beginning can save you a lot of headache later on.
When I started the development of JitBlox, I also decided that I wanted to offer undo-functionality right from the start. JitBlox is built with Angular, so a quick search for angular undo history already gave lots of useful links. However, most of the solutions I found were coupled to libraries that solved a lot more than just undo/redo. While that might be a good thing if one of these libraries fit within your architecture, I wanted to keep my implementation of such a key feature to be as much as possible framework-independent.
In this article, I will go through my design decisions while implementing my own, lightweight, undo/redo history in pure TypeScript. You can find a working implementation in a simple to-do manager app which can be found in this Git repository. Even though the demo is built with Angular, the core undo/redo functionality is framework independent (with a bit of RxJS on top of it to make things easier).
The application domain consists of just a single entity, which is named (as you might have already guessed) a
The app allows a user to add/remove to-do items and to update individual properties of a to-do item. It also demonstrates two actions that are less generic: one for stepping a numeric property (
priority) with a specified amount, and one for toggling a boolean value (
Let’s start by setting some design goals to keep in mind during the process.
- Minimize dependencies: As already pointed out, I set myself a goal to build something independent from any state management library like Redux or it’s Angular counterpart NgRx. The Redux principle can be very valuable to your application (e.g. forcing immutable state, caching), but this really depends on the type of application you are building.
- Lightweight: No snapshots of the application state. Some undo/redo solutions work by capturing the application state at each action (see the Memento pattern), meaning that objects need to be deep-cloned. While this may be ok for smaller applications, it might become inefficient when dealing with lots of data or a long undo-history.
- Benefit from TypeScript: design a strongly-typed API that is easy to understand and prevents callers from making mistakes.
- Extensible: Most undo/redo actions will apply to basic CRUD actions: adding new items to (or removing from) an array and updating properties of an object. But the solution shouldn’t be restricted to that and allow for implementation of custom, domain-specific actions.
Choosing a design pattern
It doesn’t take a lot of research to find that for implementing undo and redo, there are basically two design patterns to choose from:
- Memento pattern: A memento represents something you want to hold onto. For undo/redo, this means that an application must be able to revert to the state in which it existed before a user executed an operation.
- Command pattern: With the command pattern, you represent each operation as a unique object. As the user executes operations, corresponding command objects are stored in a history stack. As long as each command can support being “undone,” undo and redo is a simple matter of tracking back and forth through this stack (or, typically a “past” and a “future” stack).
Both patterns have their own advantages and disadvantages. Here’s a quick comparison:
Let’s start by defining the core of our design (without reinventing the wheel): the Command interface. This interface needs to be implemented by any class that performs an undoable action of any kind.
Also, I defined a
CommandResult interface so that we know if the command got executed successfully and we know if/how we can store it in the history or communicate the result back to other parts of the application.
The core of our design consists of the following types working together.
- UndoHistory: Stores executed commands in an undo- or redo-stack.
- CommandHandler: The
CommandHandleris responsible for executing commands, managing the
UndoHistoryand executing undo/redo commands.
Implementing the first command
Let’s implement the first command: one that updates a single property on a target object.
Note that this command uses
keyof(TTarget) to make sure that the property is really a property of the target object, preventing callers from providing non-existent property names.
Executing a command
Now we have our first command (more commands will follow below), executing it looks as follows:
While this works, there are some points for improvement:
- Callers must instantiate a concrete implementation of
UpdatePropertyCommand. Wouldn't it be better to add some abstraction around the implementation details?
- Callers might not even know that the
UpdatePropertyCommandexists in the first place.
So what about refactoring the
UpdatePropertyCommand a little bit and separate the command information from the actual implementation?
And update our concrete implementation as follows:
We have now opened up a way to just pass the command information to the command handler, and leave the command instantiation inside.
But how do we let the command handler know that it needs to deal with an “update property” command and not another command that happens to use similar command data? We could make an
execute... function for each command, but that would make the solution less maintainable. A better option would be to assign a key to each command and let TypeScript ensure that each key is unique.
Implementing a command map
You may have seen the concept of event maps in TypeScript. An event map is, for example, used for mapping DOM event names to their corresponding event arguments. We can use the same mechanism for relating command keys to command data:
Let’s update the signature of
commandHandler.execute(...) to use this command map:
Now, a caller can only pass a value for
key that is part of the command map, with command data that corresponds to the key.
Our command execution interface already got easier to use: callers are just talking to the
CommandHandler, which takes care of creating the right command instance, executing it and adding it to the undo history. However, this approach isn't very flexible:
- Whenever a new command is implemented, we need to update the internals of our CommandHandler implementation to make it work.
- There is no way for callers to implement their own commands.
This brings me to the last update to our design, which is to move command instantiation to a factory method and revert the
commandHandler.execute function so that it accepts any object that implements the
Now, executing a command looks as follows:
This allows the
CommandHandler to accept any object that implements the
Command interface, and we still have a strongly-typed factory method for creating common commands. Also, the factory/command map duo can be easily extended to create other (probably more domain-specific) commands:
Implementing more generic commands
Until now, I’ve only used a generic
update-property command as an example. Even for basic CRUD-applications we obviously need more than that. What about, for example, a command that updates multiple properties at the same time? Luckily they only need to be implemented once, so I've already created most of them in the demo project.
Here are some of them with their data interfaces:
A command to update multiple properties of a target object.
As you can see, the UpdatePropertiesCommand makes use of TypeScript’s built-in
Partial<T> type (a type with all properties of Type set to optional). Example usage with a
Adds an element to a target object’s child collection. The
propertyName can only be set to a property that is actually an array of a specified type.
Likewise, the example project also contains commands for removing elements from a collection (either by reference or by index).
Increments a numeric property with a specified amount. The example project also uses this command is also used to increment (or decrement) a the priority of a
TodoItem by steps of 3 (you manager will love this -:). This is a good example of a command that can be repeated infinitely.
Responding to undo/redo actions
At some point, you will need to synchronize your data with a backend. How and when to do this depends on your state management architecture, which is outside the scope of this article. The demo application demonstrates one way to plug into the undo/redo history: by making a wrapper around the
CommandHandler that makes all actions observable (using RxJS in this case).
Then, a higher-level component or service can then subscribe to this as follows:
See it all in action
You can see it all in action by cloning the demo repository and runing
npm install followed by
ng serve -o. I'm curious to know what you think (are there similar solutions out there that I've missed? are there any points for improvement?). Feel free to drop a comment.
Originally published at https://www.jitblox.com on January 22, 2021.