Overview
Extend the Vue Scheduler component with unlimited undo/redo functionality.
The Undo and Redo buttons are only enabled if the action is allowed.
See the history of all actions and the current position.
License
Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.
Introduction to the Vue Scheduler component
For an introduction to using the Vue Scheduler component please see the tutorial:
This undo/redo tutorial assumes that you are familiar with the Vue Scheduler component basics.
How to implement undo/redo for the Vue Scheduler?
The attached projects includes an UndoService
class which contains the implementation of the undo/redo logic.
You need to feed it with the initial event set and record all actions.
The service maintains a history of all actions performed.
The service can return information about the last action so you can perform the “undo” action.
It also maintains the current position in the history and if available, you can get information about the next action and perform “redo”.
Each history item stores the type of operation (add/edit/remove) and the event state before (previous) and after (current) the action. It also stores a text comment that contains details about the action.
How to record Vue Scheduler drag and drop actions in undo/redo history?
In order to perform the undo/redo, you need to use the UndoService
class and record all changes performed by the user.
First, you need to create an instance of the UndoService
:
const service = ref(new UndoService()).value;
Before you start recording the user drag and drop actions using the UndoService
, you need to save the initial state of the event data set using the initialize()
method:
const loadEvents = () => {
const events = [
{ id: 1, start: "2025-08-03T00:00:00", end: "2025-08-08T00:00:00", text: "Event 1", resource: "R4" },
{ id: 2, start: "2025-08-04T00:00:00", end: "2025-08-10T00:00:00", text: "Event 2", resource: "R2" }
];
config.events = events;
service.initialize(events);
};
// ...
onMounted(() => {
loadEvents();
// ...
});
Now you can record the actions. You need to record every change made to the events, either using the UI or using the direct API.
To record changes made by the user using the Scheduler UI, you can use the following events handlers:
onEventMoved: fired when an event has been moved
onEventResized: fired when an event has been resized
onTimeRangeSelected: fired when a time range has been selected, that is usually mapped to new event creation
onClick
event of the active area with the “delete” icon: fired when the event is removed
Here is the logic that uses the drag and drop event handlers to record the actions:
const service = ref(new UndoService()).value;
const schedulerRef = ref(null);
const scheduler = computed(() => schedulerRef.value.control);
const config = reactive({
timeHeaders: [{ groupBy: "Month" }, { groupBy: "Day", format: "d" }],
scale: "Day",
days: 31,
startDate: "2025-08-01",
timeRangeSelectedHandling: "Enabled",
durationBarVisible: false,
eventHeight: 34,
onTimeRangeSelected: async (args) => {
const control = args.control;
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
control.clearSelection();
if (modal.canceled) {
return;
}
const data = {
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
};
control.events.add(data);
service.add(data, "New event created");
},
onEventMoved: (args) => {
service.update(args.e.data, "Event moved");
},
onEventResized: (args) => {
service.update(args.e.data, "Event resized");
},
onEventDeleted: (args) => {
service.remove(args.e.data, "Event removed");
},
onBeforeEventRender: args => {
args.data.areas = [
{
top: 5,
right: 5,
width: 24,
height: 24,
padding: 3,
symbol: "/icons/daypilot.svg#x-2",
backColor: "#ed9337",
fontColor: "#fff",
style: "border-radius: 50%;",
onClick: args => {
const event = args.source;
scheduler.value.events.remove(event);
service.remove(event.data, "Event removed");
}
}
];
},
treeEnabled: true,
});
The UndoService
provides the following methods for recording the changes:
add()
update()
remove()
These methods have two parameters:
item
- the changed object (with the exception ofremove()
, it is the new state of the object); the object has to be serializable usingJSON.stringify()
text
- a text description of the change (optional)
The UndoService
saves the change in the history, keeping the old and new state. It also advances the current position in the history.
How to add an Undo button to the Vue Scheduler?
The “Undo” button state is set to disabled if the UndoService
doesn’t allow performing the “undo” action. You can read the state using canUndo
property.
<template>
<button v-on:click="undo" :disabled="!service.canUndo">Undo</button>
<DayPilotScheduler :config="config" ref="schedulerRef"/>
</template>
Once the user clicks the “Undo” button you need to get the previous operation from the undo history using undo()
method and revert the operation:
If the action was "add" you need to remove the event using
events.remove()
method.If the action was "update" you need to restore the previous event state that is stored in the
previous
property of the history item.If the action was "remove" you need to add the event back to the Vue Scheduler using
events.add()
method. The original event is stored in theprevious
property of the history item.
The undo service automatically updates the history and the current position within the history.
const undo = () => {
const item = service.undo();
switch (item.type) {
case "add":
scheduler.value.events.remove(item.id);
break;
case "remove":
scheduler.value.events.add(item.previous);
break;
case "update":
scheduler.value.events.update(item.previous);
break;
}
};
How to add a Redo button to the Vue Scheduler?
The Redo button is only enabled if the history has at least item and the current position isn’t after the last item.
<template>
<!-- ... -->
<button v-on:click="redo" :disabled="!service.canRedo">Redo</button>
<DayPilotScheduler :config="config" ref="schedulerRef"/>
</template>
After clicking the “Redo” button, you need to replay the action returned by the redo()
method of the UndoService
:
const redo = () => {
const item = service.redo();
switch (item.type) {
case "add":
scheduler.value.events.add(item.current);
break;
case "remove":
scheduler.value.events.remove(item.id);
break;
case "update":
scheduler.value.events.update(item.current);
break;
}
};
Full Source Code
Here is the full source code of our Vue Scheduler component example that implements undo/redo for drag and drop actions:
<template>
<div class="buttons">
<button v-on:click="undo" :disabled="!service.canUndo">Undo</button>
<button v-on:click="redo" :disabled="!service.canRedo">Redo</button>
</div>
<DayPilotScheduler :config="config" ref="schedulerRef"/>
<h2>History</h2>
<div v-for="(item, i) in service?.history" :key="item" v-bind:class="{highlighted: service.position === i}"
class="history-item">
{{ i }}: {{ item.type }} - {{ item.text }}
</div>
<div v-bind:class="{highlighted: service.history.length && service.position === service.history.length}"></div>
</template>
<script setup>
import {ref, onMounted, computed, reactive} from 'vue';
import { DayPilot, DayPilotScheduler } from 'daypilot-pro-vue';
import { UndoService } from "../undo/UndoService";
const service = ref(new UndoService()).value;
const schedulerRef = ref(null);
const scheduler = computed(() => schedulerRef.value.control);
const config = reactive({
timeHeaders: [{ groupBy: "Month" }, { groupBy: "Day", format: "d" }],
scale: "Day",
days: 31,
startDate: "2025-08-01",
timeRangeSelectedHandling: "Enabled",
durationBarVisible: false,
eventHeight: 34,
onTimeRangeSelected: async (args) => {
const control = args.control;
const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
control.clearSelection();
if (modal.canceled) {
return;
}
const data = {
start: args.start,
end: args.end,
id: DayPilot.guid(),
resource: args.resource,
text: modal.result
};
control.events.add(data);
service.add(data, "New event created");
},
onEventMoved: (args) => {
service.update(args.e.data, "Event moved");
},
onEventResized: (args) => {
service.update(args.e.data, "Event resized");
},
onEventDeleted: (args) => {
service.remove(args.e.data, "Event removed");
},
onBeforeEventRender: args => {
args.data.areas = [
{
top: 5,
right: 5,
width: 24,
height: 24,
padding: 3,
symbol: "/icons/daypilot.svg#x-2",
backColor: "#ed9337",
fontColor: "#fff",
style: "border-radius: 50%;",
onClick: args => {
const event = args.source;
scheduler.value.events.remove(event);
service.remove(event.data, "Event removed");
}
}
];
},
treeEnabled: true,
});
const loadEvents = () => {
const events = [
{ id: 1, start: "2025-08-03T00:00:00", end: "2025-08-08T00:00:00", text: "Event 1", resource: "R4" },
{ id: 2, start: "2025-08-04T00:00:00", end: "2025-08-10T00:00:00", text: "Event 2", resource: "R2" }
];
config.events = events;
service.initialize(events);
};
const loadResources = () => {
const resources = [
{ name: "Resource 1", id: "R1" },
{ name: "Resource 2", id: "R2" },
{ name: "Resource 3", id: "R3" },
{ name: "Resource 4", id: "R4" },
{ name: "Resource 5", id: "R5" },
{ name: "Resource 6", id: "R6" },
{ name: "Resource 7", id: "R7" },
];
config.resources = resources;
};
const undo = () => {
const item = service.undo();
switch (item.type) {
case "add":
scheduler.value.events.remove(item.id);
break;
case "remove":
scheduler.value.events.add(item.previous);
break;
case "update":
scheduler.value.events.update(item.previous);
break;
}
};
const redo = () => {
const item = service.redo();
switch (item.type) {
case "add":
scheduler.value.events.add(item.current);
break;
case "remove":
scheduler.value.events.remove(item.id);
break;
case "update":
scheduler.value.events.update(item.current);
break;
}
};
onMounted(() => {
loadResources();
loadEvents();
});
</script>
<style>
body .scheduler_default_event_inner {
border-radius: 25px;
background: #ffcc99;
color: #333;
border: 1px solid #ed9337;
padding: 5px;
}
body .scheduler_default_event {
border-radius: 25px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.scheduler_default_shadow {
border-radius: 25px;
}
.scheduler_default_shadow_inner {
border-radius: 25px;
background: #ffcc99;
color: #333;
border: 1px solid #ed9337;
padding: 5px;
}
</style>
<style scoped>
h2 {
margin-top: 10px;
margin-bottom: 30px;
padding-top: 10px;
}
.history-item {
background-color: #f9f9f9;
border: 1px solid #e0e0e0;
padding: 10px;
margin-bottom: 4px;
border-radius: 4px;
transition: background-color 0.3s, box-shadow 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.highlighted {
position: relative;
margin-top: 20px;
}
.highlighted::before {
content: 'Position';
position: absolute;
left: 20px;
top: -20px;
color: #3c78d8;
font-size: 14px;
}
.highlighted::after {
content: '';
position: absolute;
left: 5px;
top: -15px;
width: 10px;
height: 10px;
background-color: #3c78d8;
border-radius: 50%;
}
.buttons {
margin-bottom: 10px;
display: inline-flex;
}
.buttons button {
background-color: #3c78d8;
color: white;
border: 0;
padding: .5rem 1rem;
width: 80px;
cursor: pointer;
margin-right: 1px;
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(0,0,0,0.08);
box-sizing: border-box;
}
.buttons button:last-child {
margin-right: 0;
}
.buttons button.selected {
background-color: #1c4587;
box-shadow: 0 3px 5px rgba(0,0,0,0.1);
}
.buttons button:disabled {
background-color: #d3d3d3;
color: #333;
cursor: not-allowed;
}
.buttons button:first-child {
border-top-left-radius: 30px;
border-bottom-left-radius: 30px;
}
.buttons button:last-child {
border-top-right-radius: 30px;
border-bottom-right-radius: 30px;
}
.buttons button:not(:disabled):hover {
background-color: #2f66c4;
box-shadow: 0 5px 7px rgba(0,0,0,0.1);
}
.buttons button:active {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
You can find the source code of the UndoService
class in the full project that can be downloaded at the top of this tutorial.