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

MethodReturnsDescription
undo()booleanUndo the most recent transaction. Returns false when the stack is empty.
redo()booleanReplay the most recently undone transaction. Returns false when the redo stack is empty.
canUndo()booleanWhether an undoable transaction is at the top of the stack.
canRedo()booleanWhether a redoable transaction exists.
clearHistory()voidDrop 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

FieldTypeDescription
kind'record' | 'undo' | 'redo' | 'clear'What triggered the event
canUndobooleanWhether the undo stack has entries
canRedobooleanWhether the redo stack has entries
undoSizenumberCurrent undo stack depth
redoSizenumberCurrent redo stack depth
topUndoLabelstring | undefinedLabel of the top undo transaction
topRedoLabelstring | undefinedLabel of the top redo transaction
timestampnumberDate.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
  },
})
OptionTypeDefaultDescription
enabledbooleantrueWhen false, no commands are recorded and undo()/redo() are no-ops
maxSizenumber100Maximum 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 },
})