Skip to main content

Command History

Reactodia uses a common pattern to organize changes to the diagram model into "commands" representing different atomic actions to facilitate undo/redo support.

Commands and undo/redo history

The Command interface defines an action which performs a set of changes to the Reactodia state and can be executed and reverted as needed.

tip

The library contains many built-in commands to manipulate the diagram model, graph authoring state or perform other effects, you can find them all at API > workspace > Commands.

To execute or revert a command, the CommandHistory instance from DiagramModel.history should be used to be able to undo or redo it later:

function Component() {
const {model} = Reactodia.useWorkspace();

const onClick = () => {
const command = changeLinkTypeVisibility(
model, 'http://example.com/connectedTo', 'hidden'
);
model.history.execute(command);
};

const onUndo = () => {
// Later: undo or redo a command
model.history.undo();
model.history.redo();
};

// ...
}

Another way to use command history is to perform state changes and register a "revert" command which is used for diagram geometry updates:

function Component() {
const {model} = Reactodia.useWorkspace();

const onMove = () => {
const restoreGeometry = Reactodia.RestoreGeometry.capture(model);
/* ... make changes to the element positions and link vertices ... */
model.history.registerToUndo(command);
};

// ...
}

Command batches

When executing multiple commands in a sequence in cases it would be desirable to undo or redo them all at once at though they were a single atomic command. In that case it is possible to start a CommandBatch via CommandHistory.startBatch(), execute the commands and store the batch, so a single command is added to the history:

function Component() {
const {model} = Reactodia.useWorkspace();
const {canvas} = Reactodia.useCanvas();

const onAddElements = (
target: Reactodia.Element,
iris: readonly Reactodia.ElementIri[]
) => {
const batch = model.history.startBatch('Adding multiple elements');

for (const iri of iris) {
// Some methods implicitly add commands to the history,
// i.e. to the active batch if any
const element = model.createElement(iri);

// In other cases the command needs to be executed explicitly
batch.history.execute(setElementState(element, {'my:custom:state': 42}));
}

batch.store();
};
}

It is also possible to discard a batch instead of storing it with CommandBatch.discard() to avoid putting the commands in the history in the first place.

note

Starting a new batch when there is an active command batch already causes the new batch to become nested, which allows to use operations creating command batches as part of a larger operation having its own top-level batch.

How to define a new command

While it is possible to implement Command interface directly, the library provides a utility namespace with the same name to simplify the process.

Command.create() defines a command from a callback which returns the reverse command:

function exchangeElementPositions(
first: Reactodia.ElementElement,
second: Reactodia.ElementElement
): Command {
return Command.create('Exchange element positions', () => {
const position = first.position;
first.setPosition(second.position);
second.setPosition(position);
return exchangeElementPositions(first, second);
});
}

Command.compound() defines a command from a sequence of other commands similar to using a command batch:

function resetElementStateForAll(
elements: readonly Reactodia.ElementElement[]
): Command {
const commands = elements.map(el => Reactodia.setElementState(el, undefined));
return Command.compound('Reset state for elements', commands);
}

Command.effect() defines a command which runs only after it executed in "forward" direction but skipped on revert:

function logAsCommand(message: string): Command {
return Command.effect('Log a message', () => console.log(message));
}