Undo and Redo
ApexGantt records every mutating operation in an internal command stack. Drag, resize, inline edit, dialog edit, addTask, deleteTask, moveTask, addDependency, and removeDependency are all captured automatically. Call undo() to roll back and redo() to replay.
How history works
Every mutation is wrapped in a named transaction and pushed onto the undo stack. When undo() runs, the transaction pops off the undo stack and onto the redo stack. Any new mutation after an undo discards the redo stack — forward history is gone once you branch.
Multi-step operations (like deleteTask with cascade: 'children') are grouped into a single atomic transaction so a single undo restores the entire subtree.
API
| Method | Returns | Description |
|---|---|---|
undo() | boolean | Undo the most recent transaction. Returns false when the stack is empty. |
redo() | boolean | Replay the most recently undone transaction. Returns false when the redo stack is empty. |
canUndo() | boolean | Whether an undoable transaction is at the top of the stack. |
canRedo() | boolean | Whether a redoable transaction exists. |
clearHistory() | void | Drop all recorded transactions and emit a historyChange event. |
getHistorySize() | { undo: number; redo: number } | Snapshot of current stack depths. |
Basic usage
const gantt = new ApexGantt(document.getElementById('chart'), options)
// after mount
document.getElementById('undo-btn').addEventListener('click', () => {
gantt.undo()
})
document.getElementById('redo-btn').addEventListener('click', () => {
gantt.redo()
})
Keeping buttons in sync with historyChange
Rather than polling canUndo() / canRedo(), listen for the historyChange event. It fires after every mutation, undo, redo, or clearHistory() call with the current stack state.
const undoBtn = document.getElementById('undo-btn')
const redoBtn = document.getElementById('redo-btn')
gantt.el.addEventListener('historyChange', (e) => {
const { canUndo, canRedo, undoSize, redoSize, topUndoLabel } = e.detail
undoBtn.disabled = !canUndo
redoBtn.disabled = !canRedo
if (topUndoLabel) {
undoBtn.title = `Undo: ${topUndoLabel}`
}
console.log(`Stack: ${undoSize} undo / ${redoSize} redo`)
})
historyChange detail payload
| Field | Type | Description |
|---|---|---|
kind | 'record' | 'undo' | 'redo' | 'clear' | What triggered the event |
canUndo | boolean | Whether the undo stack has entries |
canRedo | boolean | Whether the redo stack has entries |
undoSize | number | Current undo stack depth |
redoSize | number | Current redo stack depth |
topUndoLabel | string | undefined | Label of the top undo transaction |
topRedoLabel | string | undefined | Label of the top redo transaction |
timestamp | number | Date.now() at the time of the event |
Keyboard shortcuts
ApexGantt listens for Ctrl+Z (undo) and Ctrl+Y or Ctrl+Shift+Z (redo) automatically when the chart container or any child has focus. The shortcuts skip when focus is inside a text input or textarea so native browser undo still works in editable fields.
No configuration is needed — the keyboard handler is always active when history is enabled.
Configuring the history stack
Pass a history object in your options to tune the stack:
const gantt = new ApexGantt(el, {
// ...
history: {
enabled: true, // default: true
maxSize: 50, // default: 100
},
})
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | When false, no commands are recorded and undo()/redo() are no-ops |
maxSize | number | 100 | Maximum undo entries. Older entries drop off the bottom (FIFO). |
Disabling history for custom history layers
If your application implements its own CRDT-backed or server-synced history, disable the built-in stack to avoid double-recording:
const gantt = new ApexGantt(el, {
history: { enabled: false },
})
// intercept mutations via events instead
gantt.el.addEventListener('taskUpdateSuccess', (e) => {
myHistoryLayer.record(e.detail)
})
With enabled: false, canUndo() and canRedo() always return false, and undo()/redo() are no-ops.
React example
import { useRef, useState, useEffect } from 'react'
import { ApexGanttChart } from 'react-apexgantt'
import type { ApexGanttHandle } from 'react-apexgantt'
export default function GanttWithHistory() {
const ganttRef = useRef<ApexGanttHandle>(null)
const [canUndo, setCanUndo] = useState(false)
const [canRedo, setCanRedo] = useState(false)
useEffect(() => {
const el = ganttRef.current?.getInstance()?.el
if (!el) return
const handler = (e: Event) => {
const { canUndo, canRedo } = (e as CustomEvent).detail
setCanUndo(canUndo)
setCanRedo(canRedo)
}
el.addEventListener('historyChange', handler)
return () => el.removeEventListener('historyChange', handler)
}, [])
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<button
disabled={!canUndo}
onClick={() => ganttRef.current?.getInstance()?.undo()}
>
Undo
</button>
<button
disabled={!canRedo}
onClick={() => ganttRef.current?.getInstance()?.redo()}
>
Redo
</button>
</div>
<ApexGanttChart
ref={ganttRef}
tasks={tasks}
options={{ enableTaskDrag: true, enableTaskResize: true }}
height="500px"
/>
</div>
)
}
Vue example
<template>
<div>
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
<button :disabled="!canUndo" @click="handleUndo">Undo</button>
<button :disabled="!canRedo" @click="handleRedo">Redo</button>
</div>
<ApexGanttChart
ref="ganttRef"
:tasks="tasks"
:options="ganttOptions"
height="500px"
@history-change="onHistoryChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ApexGanttChart } from 'vue-apexgantt'
const ganttRef = ref<InstanceType<typeof ApexGanttChart> | null>(null)
const canUndo = ref(false)
const canRedo = ref(false)
const ganttOptions = {
enableTaskDrag: true,
enableTaskResize: true,
}
const onHistoryChange = (detail: { canUndo: boolean; canRedo: boolean }) => {
canUndo.value = detail.canUndo
canRedo.value = detail.canRedo
}
const handleUndo = () => ganttRef.value?.getInstance()?.undo()
const handleRedo = () => ganttRef.value?.getInstance()?.redo()
</script>
Clearing history on data reload
When you load a fresh dataset (for example, switching between projects), clear the history so stale undo entries from the previous project cannot be replayed:
async function loadProject(projectId: string) {
const data = await fetchProject(projectId)
gantt.update({ series: data })
gantt.clearHistory()
}
Built-in toolbar buttons
When enableTaskCRUDToolbar: true, the built-in toolbar renders Undo and Redo buttons alongside the Add and Delete buttons. They are automatically disabled when the corresponding stack is empty — no additional wiring is needed.
const gantt = new ApexGantt(el, {
enableTaskCRUDToolbar: true,
enableSelection: true,
history: { maxSize: 50 },
})