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 the 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 the 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 graph (either a graph attribute or attribute of a cell) onto undoStack and it puts every reverted changes onto redoStack.

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

Batch commands

CommandManager introduces a support for batches. A batch is a group of commands that are executed in a single transaction. When a batch is executed, the CommandManager stores the whole batch as a single command. It merges the same attribute changes into a single delta and ignore the deltas that are not modifying the graph (e.g. the last change in the batch reverts the first).

commandManager.initBatchCommand();

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

commandManager.storeBatchCommand();

// The undo() will revert both changes at once.
commandManager.undo();

Note that other way to trigger a batch is to use the startBatch and stopBatch methods. This is useful when the context which is making the changes does not have access to the CommandManager instance.

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 both changes at once.
commandManager.undo();

The batches can be nested. The changes are stored on the undo stack only when the outer batch is stopped.

graph.startBatch('outer-batch');

graph.startBatch('nested-batch-1');
const [el1, el2] = graph.getElements();
el1.set('attribute', 'value1');
el2.set('attribute', 'value2');
graph.stopBatch('nested-batch-1');
// The changes are not available for undo/redo yet.
// They are stored on the undo stack only when the outer batch is stopped.

graph.startBatch('nested-batch-2');
const [el3, el4] = graph.getElements();
el3.set('attribute', 'value3');
el4.set('attribute', 'value4');
graph.stopBatch('nested-batch-2');
// The changes are not available for undo/redo yet.

graph.stopBatch('outer-batch');
// All 4 changes are stored on the undo stack as a single command now.

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(() => commandManager.undo());
redoButtonEl.addEventListener(() => commandManager.redo());

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

Filtering commands

You can filter out commands that you don't want to be stored in the undo/redo stacks. For example, you might want to ignore changes that are accompanied by a specific data.

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

// ...

// Note that 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 as well as changes made to individual cells. If you don't want to store changes made to the graph, you can filter them out using the cmdBeforeAdd 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() do not changes the graph 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 when the changes are reverted.
  • use the applyOptionsList option to specify which options should be sent when the changes are re-applied.
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 user change. For example, while the user is dragging an element, the graph is continuously modified, but you only want to send one request to the database after the user is done. The command manager detects batches and can notify you when a batch of commands 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 graph as shown in the Tournament brackets example.