Skip to main content

Undo & Redo

The plugin that keeps track of graph changes and allows you to travel the history of those changes back and forth is called CommandManager.

Installation​

Import CommandManager from the dia namespace and create a new instance of it. Pass an instance of a dia.Graph to the constructor.

import { dia, shapes } from '@joint/plus';

const graph = new dia.Graph({}, { cellNamespace: shapes });
const commandManager = new dia.CommandManager({ graph });
There is also a UMD version available

Include joint.dia.command.js to your HTML:

index.html
<script src="joint.js"></script>
<script src="joint.dia.command.js"></script>

And access CommandManager through the joint.dia namespace:

index.js
const graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes });
const commandManager = new joint.dia.CommandManager({ graph });

How does CommandManager work?​

CommandManager listens to all graph changes and stores the delta of each change (a command). It stores every change made on the graph (either an attribute of the graph itself or an attribute of a graph cell) onto undoStack, and puts every reverted change onto redoStack.

It allows you to revert/rewind the changes stored in those stacks.

Batch commands​

CommandManager also introduces support for batches. A batch is a group of commands which are executed in a single transaction. You can start a batch with commandManager.initBatchCommand() and complete it with commandManager.storeBatchCommand().

When a batch is completed, CommandManager stores the whole batch as a single command. It merges the same attribute changes into a single delta and ignores the deltas that end up not modifying the graph (for example, when a later change in a batch reverts an earlier change).

const graph = new dia.Graph({}, { cellNamespace: shapes });
const commandManager = new dia.CommandManager({ graph });
// ... add elements to `graph`

commandManager.initBatchCommand();
const [el1, el2] = graph.getElements();
el1.set('attribute', 'value1');
el2.set('attribute', 'value2');
commandManager.storeBatchCommand();

// The `undo()` will revert `el1` and `el2` changes at once.
commandManager.undo();
There is also another way to trigger a batch...

You can use the graph.startBatch() and graph.stopBatch() methods to start and complete a batch. This is useful when the context which is making the changes does not have access to the CommandManager instance:

const graph = new dia.Graph({}, { cellNamespace: shapes });
const commandManager = new dia.CommandManager({ graph });
// ... add elements to `graph`

graph.startBatch('my-batch');
const [el1, el2] = graph.getElements();
el1.set('attribute', 'value1');
el2.set('attribute', 'value2');
graph.stopBatch('my-batch');

// The `undo()` will revert `el1` and `el2` changes at once.
commandManager.undo();

Nested batches​

CommandManager batches can be nested. The changes are stored on the undo stack only when the outermost batch is completed.

const graph = new dia.Graph({}, { cellNamespace: shapes });
const commandManager = new dia.CommandManager({ graph });
// ... add elements to `graph`

commandManager.initBatchCommand(); // outer batch

commandManager.initBatchCommand();
const [el1, el2] = graph.getElements();
el1.set('attribute', 'value1');
el2.set('attribute', 'value2');
commandManager.storeBatchCommand();
// These 2 changes are not available for undo/redo yet.
// They are stored on the undo stack only when the outer batch is completed.

commandManager.initBatchCommand();
const [el3, el4] = graph.getElements();
el3.set('attribute', 'value3');
el4.set('attribute', 'value4');
commandManager.storeBatchCommand();
// These 2 changes are not available for undo/redo yet.
// They are also stored on the undo stack only when the outer batch is completed.

commandManager.storeBatchCommand(); // outer batch
// All 4 changes are stored on the undo stack as a single command now.

// The `undo()` will revert `el1`, `el2`, `el3` and `el4` changes at once.
commandManager.undo();

Add undo/redo to toolbar​

Here's an example how to use ui.Toolbar component to create a toolbar with built-in undo and redo buttons:

import { dia, shapes, ui } from '@joint/plus';

const graph = new dia.Graph({}, { cellNamespace: shapes });
const commandManager = new dia.CommandManager({ graph });

const toolbar = new ui.Toolbar({
references: {
commandManager
},
autoToggle: true,
tools: ['undo', 'redo']
});
toolbar.render();
document.body.appendChild(toolbar.el);

For more information about the Toolbar component, see the Toolbar documentation.

A simple example of how to add undo and redo buttons to a custom toolbar can look like this:

index.html
<button id="undo-button" disabled>Undo</button>
<button id="redo-button" disabled>Redo</button>
index.js
import { dia, shapes } from '@joint/plus';

const graph = new dia.Graph({}, { cellNamespace: shapes });
const commandManager = new dia.CommandManager({ graph });

const undoButtonEl = document.getElementById('undo-button');
const redoButtonEl = document.getElementById('redo-button');

// Perform undo/redo when the buttons are clicked.
undoButtonEl.addEventListener('click', () => commandManager.undo());
redoButtonEl.addEventListener('click', () => commandManager.redo());

commandManager.on('stack', () => {
// Enable/disable the buttons based on the state of the undo/redo stacks.
undoButtonEl.disabled = !commandManager.hasUndo();
redoButtonEl.disabled = !commandManager.hasRedo();
});

Filtering commands​

You can filter out commands that you don't want stored in the undo/redo stacks using the cmdBeforeAdd() callback option. For example, you might want to ignore changes identified by a specific option (in this case, options.ignoreCommandManager):

const commandManager = new joint.dia.CommandManager({
graph,
cmdBeforeAdd: (cmdName, cell, collection, options = {}) => {
return !options.ignoreCommandManager;
}
});

// The last argument to `set()` is an options object that gets passed to the `cmdBeforeAdd()` function.
element.set({ foo: 'bar' }, { ignoreCommandManager: true });

Graph vs. cells changes​

The CommandManager stores changes made to the graph itself, as well as changes made to individual cells. If you don't want to store changes made to the graph itself, you can filter them out using the cmdBeforeAdd() callback option:

const commandManager = new dia.CommandManager({
graph,
cmdBeforeAdd: (cmdName, cell, collection, options = {}) => {
return (cell !== graph);
}
});

/* This change will NOT be stored in the undo stack */
graph.set('myGraphAttribute', 'value');

/* This change will be stored in the undo stack */
const [el1] = graph.getElements();
el1.set('myElementAttribute', 'value');

Passing data along with the undo/redo changes​

By default the undo() and redo() functions do not reinstate graph changes with the same options that were used when the command was added to the undo/redo stack. However, you can specify which options should be used:

  • Use the revertOptionsList option to specify which options should be sent along when the changes are reverted on undo().
  • Use the applyOptionsList option to specify which options should be sent along when the changes are re-applied on redo().
const commandManager = new dia.CommandManager({
/* ... */
revertOptionsList: ['myOptionAttribute1'],
applyOptionsList: ['myOptionAttribute2']
});

element.set('attribute', 'value', { myOptionAttribute1: 5, myOptionAttribute2: 6 });

/* `undo()` calls `element.set('attribute', 'prevValue', { myOptionAttribute1: 5 });` */
commandManager.undo();

/* `redo()` calls `element.set('attribute', 'value', { myOptionAttribute2: 6 });` */
commandManager.redo();

Other use cases​

Detecting graph changes​

The command manager is also useful if you want to automatically save the diagram after each meaningful user change. For example, you may want to send a request to a database every time the user is done dragging an element in your diagram. Even though the graph is continuously modified during dragging, the command manager is able to detect batches and notify you only when a batch is complete.

commandManager.on('stack', () => {
// serialize the graph and send it to the database
sendGraphToDatabase(graph.toJSON());
});

In the Chatbot example, the command manager is used to update the JSON representation of the graph after each user interaction.

Rewind / Fast forward​

You can also use the command manager to rewind or fast forward the graph to a specific state.

For example, you can add a slider to the UI that allows the user to move back and forth in the history of the diagram, as shown in the Tournament brackets example.