Skip to main content

Undo and Redo

This guide covers how to give users a local undo/redo stack for the changes they make to events - creating, editing, dragging, and deleting - and where that stack stops short of your backend.

PRO

The functionality is available in PRO Edition only

Overview

Undo/redo is opt-in. Set history to true on <Calendar> and the store starts recording event mutations as they pass through the event bus. Each entry keeps a snapshot of the whole local event collection, so undo restores the calendar to how it looked before the last tracked change and redo reapplies it.

Two things are worth keeping in mind before you wire it up:

  • History is about local store state, not your server. Undo and redo swap event records in place - they don't replay inverse actions, so a data provider never sees them. More on that below.
  • Snapshots cover the entire local collection. That's why anything that replaces the data wholesale (a fresh load, a provider push) clears the stacks - keeping stale snapshots around could let a later undo wipe out newer records.

The reactive state exposes how many steps are available in each direction as history: { undo, redo }. Read it to drive your own controls:

<script setup>
import { ref, computed } from "vue";

const api = ref();
const state = computed(() => api.value?.getReactiveState());
</script>

<template>
<button v-if="state?.history.undo" :onclick="() => api.exec('undo', {})">Undo</button>
</template>

The undo and redo actions both take an empty payload. Dispatch them through api.exec, or let the toolbar do it for you.

Enabling history

Turn on the prop and you're done - no per-event setup:

<script setup>
import { ref } from "vue";
import { Calendar, Editor } from "@svar-ui/vue-calendar";

const api = ref();
const events = [
{
id: 1,
text: "Design review",
start: new Date("2026-03-02T10:00"),
end: new Date("2026-03-02T11:00"),
},
];
</script>

<template>
<Calendar ref="api" :events="events" history :date="new Date('2026-03-02')" />
<Editor v-if="api" :api="api" />
</template>

From here, every add, edit, move, or delete the user makes is recorded, and the default toolbar shows Undo and Redo controls.

Tracked actions and coalescing

Only the four event-mutation actions are undoable:

Navigation, selection, filtering, and external-data actions are not tracked - undo won't step back through a view change or a filter. A new tracked mutation clears the redo stack, same as any editor.

To keep the stack from filling up with near-identical entries, the store coalesces bursts of related changes into one step:

  • Consecutive update-event actions for the same event id merge for 5 seconds. Dragging a title through a few keystrokes, or nudging start/end times in quick succession, undoes as a single operation back to the state before the first edit.
  • Consecutive actions of the same kind (add-event, move-event, or delete-event) merge for 250 ms. This folds a tight multi-event burst - say, a bulk delete - into one undo.

Toolbar controls

With history on and the default toolbar, Undo and Redo icon buttons appear before the Add Event button. Their disabled state tracks the history counts automatically, so they gray out when there's nothing left to undo or redo.

If you build a custom toolbar, add top-level items with id: "undo" and id: "redo" to get the same wiring and disabled-state handling:

const toolbar = {
items: [
{ id: "nav", comp: "dateNav" },
{ comp: "spacer" },
{ id: "undo", comp: "icon", icon: "wxi-undo" },
{ id: "redo", comp: "icon", icon: "wxi-redo" },
{ id: "add-event", comp: "addEventButton" },
],
};

In readonly mode the undo and redo items are stripped from the rendered toolbar, alongside the add-event button - a read-only calendar has nothing to undo.

Invalidation and the persistence boundary

Two things clear both stacks: init() on the store and every provide-data action. Because a snapshot covers the whole local collection, retaining it across an authoritative reload or a dynamically loaded page of data could make a later undo discard the newer records. Clearing on data replacement avoids that.

The bigger boundary is your backend. Undo and redo restore local store records directly - they don't synthesize inverse add-event, update-event, or delete-event actions. So when you persist through RestDataProvider, a restored snapshot never reaches the server: the provider only sees the original mutations, not the undo that reverted them.

If you need undo to round-trip to a backend, handle the undo and redo actions yourself and apply your own diff protocol:

api.on("undo", () => {
// compare api.getEvents() against your last known server state
// and push the delta with your own requests
});