diff --git a/applications/UIWidgets/Guides/SchedulerLegacyForm/index.css b/applications/UIWidgets/Guides/SchedulerLegacyForm/index.css
new file mode 100644
index 0000000000..9523cbf80c
--- /dev/null
+++ b/applications/UIWidgets/Guides/SchedulerLegacyForm/index.css
@@ -0,0 +1,3 @@
+#scheduler {
+ height: 600px;
+}
diff --git a/applications/UIWidgets/Guides/SchedulerLegacyForm/index.html b/applications/UIWidgets/Guides/SchedulerLegacyForm/index.html
new file mode 100644
index 0000000000..5bda7d8c0e
--- /dev/null
+++ b/applications/UIWidgets/Guides/SchedulerLegacyForm/index.html
@@ -0,0 +1 @@
+
diff --git a/applications/UIWidgets/Guides/SchedulerLegacyForm/index.js b/applications/UIWidgets/Guides/SchedulerLegacyForm/index.js
new file mode 100644
index 0000000000..164cd99a0c
--- /dev/null
+++ b/applications/UIWidgets/Guides/SchedulerLegacyForm/index.js
@@ -0,0 +1,257 @@
+$(function () {
+ const assignees = [
+ { text: 'Samantha Bright', id: 1, color: '#727bd2' },
+ { text: 'John Heart', id: 2, color: '#32c9ed' },
+ { text: 'Todd Hoffman', id: 3, color: '#2a7ee4' },
+ { text: 'Sandra Johnson', id: 4, color: '#7b49d3' },
+ ];
+
+ const rooms = [
+ { text: 'Room 1', id: 1, color: '#00af2c' },
+ { text: 'Room 2', id: 2, color: '#56ca85' },
+ { text: 'Room 3', id: 3, color: '#8ecd3c' },
+ ];
+
+ const priorities = [
+ { text: 'High', id: 1, color: '#cc5c53' },
+ { text: 'Low', id: 2, color: '#ff9747' },
+ ];
+
+ const resources = [
+ { fieldExpr: 'roomId', dataSource: rooms, label: 'Room' },
+ { fieldExpr: 'priorityId', dataSource: priorities, label: 'Priority' },
+ { fieldExpr: 'assigneeId', allowMultiple: true, dataSource: assignees, label: 'Assignee' },
+ ];
+
+ const schedulerData = [
+ {
+ text: 'Watercolor Landscape',
+ assigneeId: [4], roomId: 1, priorityId: 2,
+ startDate: new Date('2026-04-27T17:30:00.000Z'),
+ endDate: new Date('2026-04-27T19:00:00.000Z'),
+ recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10',
+ },
+ {
+ text: 'Website Re-Design Plan',
+ assigneeId: [4], roomId: 1, priorityId: 2,
+ startDate: new Date('2026-04-27T16:30:00.000Z'),
+ endDate: new Date('2026-04-27T18:30:00.000Z'),
+ },
+ {
+ text: 'Book Flights to San Fran for Sales Trip',
+ assigneeId: [2], roomId: 2, priorityId: 1,
+ startDate: new Date('2026-04-27T19:00:00.000Z'),
+ endDate: new Date('2026-04-27T20:00:00.000Z'),
+ allDay: true,
+ },
+ {
+ text: 'Install New Router in Dev Room',
+ assigneeId: [1], roomId: 1, priorityId: 2,
+ startDate: new Date('2026-04-27T21:30:00.000Z'),
+ endDate: new Date('2026-04-27T22:30:00.000Z'),
+ },
+ {
+ text: 'Approve Personal Computer Upgrade Plan',
+ assigneeId: [3], roomId: 2, priorityId: 2,
+ startDate: new Date('2026-04-28T17:00:00.000Z'),
+ endDate: new Date('2026-04-28T18:00:00.000Z'),
+ },
+ {
+ text: 'Final Budget Review',
+ assigneeId: [1], roomId: 1, priorityId: 1,
+ startDate: new Date('2026-04-28T19:00:00.000Z'),
+ endDate: new Date('2026-04-28T20:35:00.000Z'),
+ },
+ {
+ text: 'New Brochures',
+ assigneeId: [4], roomId: 3, priorityId: 2,
+ startDate: new Date('2026-04-28T21:30:00.000Z'),
+ endDate: new Date('2026-04-28T22:45:00.000Z'),
+ },
+ {
+ text: 'Install New Database',
+ assigneeId: [2], roomId: 3, priorityId: 1,
+ startDate: new Date('2026-04-29T16:45:00.000Z'),
+ endDate: new Date('2026-04-29T18:15:00.000Z'),
+ },
+ {
+ text: 'Approve New Online Marketing Strategy',
+ assigneeId: [4], roomId: 2, priorityId: 1,
+ startDate: new Date('2026-04-29T19:00:00.000Z'),
+ endDate: new Date('2026-04-29T21:00:00.000Z'),
+ },
+ {
+ text: 'Upgrade Personal Computers',
+ assigneeId: [2], roomId: 2, priorityId: 2,
+ startDate: new Date('2026-04-29T22:15:00.000Z'),
+ endDate: new Date('2026-04-29T23:30:00.000Z'),
+ },
+ {
+ text: 'Customer Workshop',
+ assigneeId: [3], roomId: 3, priorityId: 1,
+ startDate: new Date('2026-04-30T18:00:00.000Z'),
+ endDate: new Date('2026-04-30T19:00:00.000Z'),
+ allDay: true,
+ },
+ {
+ text: 'Prepare 2026 Marketing Plan',
+ assigneeId: [1], roomId: 1, priorityId: 2,
+ startDate: new Date('2026-04-30T18:00:00.000Z'),
+ endDate: new Date('2026-04-30T20:30:00.000Z'),
+ },
+ {
+ text: 'Brochure Design Review',
+ assigneeId: [4], roomId: 1, priorityId: 1,
+ startDate: new Date('2026-04-30T21:00:00.000Z'),
+ endDate: new Date('2026-04-30T22:30:00.000Z'),
+ },
+ {
+ text: 'Create Icons for Website',
+ assigneeId: [3], roomId: 3, priorityId: 1,
+ startDate: new Date('2026-05-01T17:00:00.000Z'),
+ endDate: new Date('2026-05-01T18:30:00.000Z'),
+ },
+ {
+ text: 'Upgrade Server Hardware',
+ assigneeId: [4], roomId: 2, priorityId: 2,
+ startDate: new Date('2026-05-01T21:30:00.000Z'),
+ endDate: new Date('2026-05-01T23:00:00.000Z'),
+ },
+ {
+ text: 'Submit New Website Design',
+ assigneeId: [1], roomId: 1, priorityId: 2,
+ startDate: new Date('2026-05-01T23:30:00.000Z'),
+ endDate: new Date('2026-05-02T01:00:00.000Z'),
+ },
+ {
+ text: 'Launch New Website',
+ assigneeId: [2], roomId: 3, priorityId: 1,
+ startDate: new Date('2026-05-01T19:20:00.000Z'),
+ endDate: new Date('2026-05-01T21:00:00.000Z'),
+ },
+ ];
+
+ const legacyFormItems = [
+ {
+ name: 'mainGroup',
+ cssClass: '',
+ items: [
+ 'subjectGroup',
+ 'dateGroup',
+ {
+ name: 'repeatGroup',
+ items: [
+ 'repeatIcon',
+ {
+ name: 'customRepeatEditor',
+ editorType: 'dxSwitch',
+ dataField: 'repeat',
+ label: { text: 'Repeat' },
+ editorOptions: {},
+ },
+ ],
+ },
+ 'resourcesGroup',
+ 'descriptionGroup',
+ ],
+ },
+ {
+ name: 'recurrenceGroup',
+ itemType: 'group',
+ cssClass: '',
+ visible: false,
+ items: [
+ 'recurrenceRuleGroup',
+ 'recurrenceEndGroup',
+ ],
+ },
+ ];
+
+ function applyRepeatState(form, isRepeat) {
+ if (isRepeat) {
+ const recurrenceRule = form.option('formData') && form.option('formData').recurrenceRule;
+ form.option('colCount', 2);
+ form.option('formData.recurrenceRule', recurrenceRule || 'FREQ=DAILY');
+ form.itemOption('recurrenceGroup', 'visible', true);
+ } else {
+ form.option('colCount', 1);
+ form.option('formData.recurrenceRule', '');
+ form.itemOption('recurrenceGroup', 'visible', false);
+ }
+ }
+
+ $('#scheduler').dxScheduler({
+ dataSource: schedulerData,
+ resources: resources,
+ height: 600,
+ views: ['day', 'week', 'workWeek', 'month'],
+ currentView: 'workWeek',
+ currentDate: new Date(2026, 3, 29),
+
+ onAppointmentAdding: function (e) {
+ delete e.appointmentData.repeat;
+ },
+ onAppointmentUpdating: function (e) {
+ delete e.newData.repeat;
+ },
+
+ onAppointmentFormOpening: function (e) {
+ const form = e.form;
+
+ form.on('fieldDataChanged', function (fe) {
+ if (fe.dataField === 'recurrenceRule') {
+ form.option('formData.repeat', !!fe.value);
+ }
+ });
+
+ const hasRecurrence = !!(e.appointmentData && e.appointmentData.recurrenceRule);
+ form.option('formData.repeat', hasRecurrence);
+ applyRepeatState(form, hasRecurrence);
+
+ form.itemOption('mainGroup.repeatGroup.customRepeatEditor', 'editorOptions', {
+ onValueChanged: function (ve) {
+ applyRepeatState(form, ve.value);
+ },
+ });
+ },
+
+ editing: {
+ allowAdding: true,
+ allowUpdating: true,
+ allowDeleting: true,
+ allowDragging: true,
+ allowResizing: true,
+ allowTimeZoneEditing: false,
+ popup: {
+ maxWidth: 800,
+ toolbarItems: [
+ {
+ toolbar: 'top',
+ location: 'before',
+ text: 'Edit Appointment',
+ cssClass: 'dx-toolbar-label',
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'done',
+ options: {
+ stylingMode: 'contained',
+ type: 'default',
+ text: 'Save',
+ },
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'cancel',
+ },
+ ],
+ },
+ form: {
+ iconsShowMode: 'none',
+ items: legacyFormItems,
+ },
+ },
+ });
+});
diff --git a/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.css b/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.css
new file mode 100644
index 0000000000..8a8cc0a5dc
--- /dev/null
+++ b/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.css
@@ -0,0 +1,10 @@
+#scheduler {
+ height: 600px;
+}
+
+/* "Discard Changes" button inside the appointment edit form */
+.discard-btn-item {
+ padding-top: 8px !important;
+ border-top: 1px solid #eee;
+ margin-top: 4px;
+}
diff --git a/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.html b/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.html
new file mode 100644
index 0000000000..5bda7d8c0e
--- /dev/null
+++ b/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.html
@@ -0,0 +1 @@
+
diff --git a/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js b/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js
new file mode 100644
index 0000000000..8c2092c846
--- /dev/null
+++ b/applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js
@@ -0,0 +1,222 @@
+$(function () {
+ const appointments = [
+ {
+ id: 1,
+ text: 'Team Stand-up',
+ startDate: new Date(2026, 4, 8, 9, 0),
+ endDate: new Date(2026, 4, 8, 9, 30),
+ description: 'Daily morning sync with the full team.'
+ },
+ {
+ id: 2,
+ text: 'Design Review',
+ startDate: new Date(2026, 4, 8, 11, 0),
+ endDate: new Date(2026, 4, 8, 12, 0),
+ description: 'Review latest UI mockups and prototypes.'
+ },
+ {
+ id: 3,
+ text: 'Sprint Planning',
+ startDate: new Date(2026, 4, 11, 14, 0),
+ endDate: new Date(2026, 4, 11, 16, 0),
+ description: 'Plan tasks for the upcoming sprint.'
+ },
+ {
+ id: 4,
+ text: 'Client Demo',
+ startDate: new Date(2026, 4, 12, 10, 0),
+ endDate: new Date(2026, 4, 12, 11, 30),
+ description: 'Showcase new features to the client.'
+ },
+ {
+ id: 5,
+ text: 'Retrospective',
+ startDate: new Date(2026, 4, 13, 15, 0),
+ endDate: new Date(2026, 4, 13, 16, 0),
+ description: 'Sprint retrospective — what went well / what to improve.'
+ }
+ ];
+ const DRAFT_PREFIX = 'dx-scheduler-draft-';
+
+ function getDraftKey(appointmentId) {
+ return DRAFT_PREFIX + (appointmentId != null ? appointmentId : 'new');
+ }
+
+ function saveDraft(appointmentId, formData) {
+ const key = getDraftKey(appointmentId);
+ const serializable = {
+ text: formData.text,
+ description: formData.description,
+ startDate: formData.startDate instanceof Date
+ ? formData.startDate.toISOString()
+ : formData.startDate,
+ endDate: formData.endDate instanceof Date
+ ? formData.endDate.toISOString()
+ : formData.endDate,
+ allDay: formData.allDay || false
+ };
+ localStorage.setItem(key, JSON.stringify(serializable));
+ }
+
+ function loadDraft(appointmentId) {
+ const key = getDraftKey(appointmentId);
+ const raw = localStorage.getItem(key);
+ if (!raw) return null;
+ const data = JSON.parse(raw);
+ if (data.startDate) data.startDate = new Date(data.startDate);
+ if (data.endDate) data.endDate = new Date(data.endDate);
+ return data;
+ }
+
+ function clearDraft(appointmentId) {
+ localStorage.removeItem(getDraftKey(appointmentId));
+ }
+ let isSaved = false;
+ let currentAppointmentId = null;
+ let popupHidingHandler = null;
+ let currentPopup = null;
+
+
+ function onCanceled(form, originalData) {
+ if (isSaved) return;
+
+ const formData = form.option('formData');
+ const appointmentId = originalData && originalData.id != null
+ ? originalData.id
+ : null;
+
+ saveDraft(appointmentId, formData);
+
+ DevExpress.ui.notify({
+ message: 'Draft saved. Your unsaved changes will be restored next time you open this appointment.',
+ type: 'warning',
+ displayTime: 4500,
+ position: { my: 'bottom center', at: 'bottom center', of: window }
+ });
+ }
+ $('#scheduler').dxScheduler({
+ dataSource: appointments,
+ currentDate: new Date(2026, 4, 8),
+ currentView: 'week',
+ views: ['day', 'week', 'month'],
+ startDayHour: 8,
+ endDayHour: 19,
+ height: 600,
+ editing: {
+ form: {
+ labelMode: 'floating',
+ items: [
+ {
+ itemType: 'group',
+ name: 'mainGroup',
+ items: [
+ 'subjectGroup',
+ 'dateGroup',
+ 'descriptionGroup',
+ 'repeatGroup',
+ 'resourcesGroup'
+ ]
+ },
+ {
+ itemType: 'group',
+ name: 'recurrenceGroup'
+ }
+ ]
+ }
+ },
+ onAppointmentFormOpening: function (e) {
+ const form = e.form;
+ const popup = e.popup;
+ const appointmentData = e.appointmentData || {};
+
+ isSaved = false;
+ currentAppointmentId = appointmentData.id != null ? appointmentData.id : null;
+ const draft = loadDraft(currentAppointmentId);
+ if (draft) {
+ const mergedData = $.extend({}, form.option('formData'), {
+ text: draft.text,
+ description: draft.description,
+ startDate: draft.startDate,
+ endDate: draft.endDate,
+ allDay: draft.allDay
+ });
+ form.option('formData', mergedData);
+ const items = form.option('items');
+ const hasDiscardBtn = items.some(function (item) {
+ return item.name === 'discardChangesButton';
+ });
+
+ if (!hasDiscardBtn) {
+ const originalData = $.extend({}, appointmentData);
+
+ items.push({
+ itemType: 'button',
+ name: 'discardChangesButton',
+ horizontalAlignment: 'left',
+ cssClass: 'discard-btn-item',
+ buttonOptions: {
+ text: 'Discard Changes',
+ type: 'danger',
+ stylingMode: 'outlined',
+ icon: 'undo',
+ onClick: function () {
+ clearDraft(currentAppointmentId);
+ isSaved = true;
+
+ form.option('formData', $.extend({}, form.option('formData'), {
+ text: originalData.text,
+ description: originalData.description,
+ startDate: originalData.startDate,
+ endDate: originalData.endDate,
+ allDay: originalData.allDay || false
+ }));
+
+ form.option('items', form.option('items').filter(function (item) {
+ return item.name !== 'discardChangesButton';
+ }));
+
+ DevExpress.ui.notify({
+ message: 'Changes discarded. Form reset to the last saved state.',
+ type: 'success',
+ displayTime: 3000
+ });
+ }
+ }
+ });
+
+ form.option('items', items);
+ }
+
+ DevExpress.ui.notify({
+ message: 'Unsaved draft restored. Use "Discard Changes" to revert to the saved appointment.',
+ type: 'info',
+ displayTime: 4000,
+ position: { my: 'bottom center', at: 'bottom center', of: window }
+ });
+ }
+ if (currentPopup && popupHidingHandler) {
+ currentPopup.off('hiding', popupHidingHandler);
+ }
+
+ currentPopup = popup;
+ popupHidingHandler = function () {
+ if (!isSaved) {
+ onCanceled(form, appointmentData);
+ }
+ };
+
+ popup.on('hiding', popupHidingHandler);
+ },
+
+ onAppointmentAdding: function () {
+ isSaved = true;
+ clearDraft(null);
+ },
+
+ onAppointmentUpdating: function (e) {
+ isSaved = true;
+ const appointmentId = e && e.oldData && e.oldData.id != null ? e.oldData.id : null;
+ clearDraft(appointmentId);
+ }
+ });
+});
diff --git a/concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md b/concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md
new file mode 100644
index 0000000000..f75432e953
--- /dev/null
+++ b/concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md
@@ -0,0 +1,643 @@
+Unsaved changes are lost when a user cancels the appointment edit form. To prevent data loss, you can save the current form state to `localStorage` when the form closes without saving, and restore the draft the next time the user opens the same appointment.
+
+
+
+### Save a Draft on Cancel
+
+The Scheduler does not expose a dedicated cancel event. To detect cancellation, listen to the Popup's [hiding](https://js.devexpress.com/Documentation/ApiReference/UI_Components/dxPopup/Configuration/#onHiding) event inside [onAppointmentFormOpening](/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#onAppointmentFormOpening). Guard with an `isSaved` flag so that the handler fires only when the user closes the form without saving:
+
+---
+##### jQuery
+
+
+ let isSaved = false;
+ let hidingHandler = null;
+
+ function onCanceled(form, originalData) {
+ const formData = form.option('formData');
+ const appointmentId = originalData && originalData.id != null
+ ? originalData.id
+ : null;
+
+ // Serialize dates as ISO strings for JSON compatibility
+ const draft = {
+ text: formData.text,
+ description: formData.description,
+ startDate: formData.startDate instanceof Date
+ ? formData.startDate.toISOString()
+ : formData.startDate,
+ endDate: formData.endDate instanceof Date
+ ? formData.endDate.toISOString()
+ : formData.endDate,
+ allDay: formData.allDay || false,
+ };
+ localStorage.setItem('dx-scheduler-draft-' + (appointmentId ?? 'new'), JSON.stringify(draft));
+ }
+
+ $('#scheduler').dxScheduler({
+ // ...
+ onAppointmentFormOpening: function (e) {
+ const form = e.form;
+ const popup = e.popup;
+ const appointmentData = e.appointmentData || {};
+
+ isSaved = false;
+
+ popup.off('hiding', hidingHandler);
+ hidingHandler = function () {
+ if (!isSaved) {
+ onCanceled(form, appointmentData);
+ }
+ };
+ popup.on('hiding', hidingHandler);
+ },
+ });
+
+##### Angular
+
+
+
+
+
+ import { Component } from '@angular/core';
+ import { DxSchedulerModule, type DxSchedulerTypes } from 'devextreme-angular/ui/scheduler';
+
+ @Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ standalone: true,
+ imports: [DxSchedulerModule],
+ })
+ export class AppComponent {
+ private isSaved = false;
+ private hidingHandler: (() => void) | null = null;
+
+ private onCanceled(
+ form: DxSchedulerTypes.AppointmentFormOpeningEvent['form'],
+ originalData: Record,
+ ): void {
+ const formData = form.option('formData') as Record;
+ const appointmentId = originalData?.['id'] != null ? originalData['id'] : null;
+
+ const draft = {
+ text: formData['text'],
+ description: formData['description'],
+ startDate: formData['startDate'] instanceof Date
+ ? (formData['startDate'] as Date).toISOString()
+ : formData['startDate'],
+ endDate: formData['endDate'] instanceof Date
+ ? (formData['endDate'] as Date).toISOString()
+ : formData['endDate'],
+ allDay: formData['allDay'] ?? false,
+ };
+ localStorage.setItem(
+ `dx-scheduler-draft-${appointmentId ?? 'new'}`,
+ JSON.stringify(draft),
+ );
+ }
+
+ handleAppointmentFormOpening(e: DxSchedulerTypes.AppointmentFormOpeningEvent): void {
+ const { form, popup } = e;
+ const appointmentData = (e.appointmentData ?? {}) as Record;
+
+ this.isSaved = false;
+
+ if (this.hidingHandler) {
+ popup.off('hiding', this.hidingHandler);
+ }
+ this.hidingHandler = () => {
+ if (!this.isSaved) {
+ this.onCanceled(form, appointmentData);
+ }
+ };
+ popup.on('hiding', this.hidingHandler);
+ }
+ }
+
+##### Vue
+
+
+
+
+
+
+
+
+##### React
+
+
+ import { useRef, useCallback } from 'react';
+ import { Scheduler, type SchedulerTypes } from 'devextreme-react/scheduler';
+
+ function onCanceled(
+ form: SchedulerTypes.AppointmentFormOpeningEvent['form'],
+ originalData: Record,
+ isSaved: React.MutableRefObject,
+ ): void {
+ if (isSaved.current) return;
+
+ const formData = form.option('formData') as Record;
+ const appointmentId = originalData?.['id'] != null ? originalData['id'] : null;
+
+ const draft = {
+ text: formData['text'],
+ description: formData['description'],
+ startDate: formData['startDate'] instanceof Date
+ ? (formData['startDate'] as Date).toISOString()
+ : formData['startDate'],
+ endDate: formData['endDate'] instanceof Date
+ ? (formData['endDate'] as Date).toISOString()
+ : formData['endDate'],
+ allDay: formData['allDay'] ?? false,
+ };
+ localStorage.setItem(
+ `dx-scheduler-draft-${appointmentId ?? 'new'}`,
+ JSON.stringify(draft),
+ );
+ }
+
+ function App() {
+ const isSaved = useRef(false);
+ const hidingHandlerRef = useRef<(() => void) | null>(null);
+
+ const handleAppointmentFormOpening = useCallback(
+ (e: SchedulerTypes.AppointmentFormOpeningEvent) => {
+ const { form, popup } = e;
+ const appointmentData = (e.appointmentData ?? {}) as Record;
+
+ isSaved.current = false;
+
+ if (hidingHandlerRef.current) {
+ popup.off('hiding', hidingHandlerRef.current);
+ }
+ hidingHandlerRef.current = () => {
+ onCanceled(form, appointmentData, isSaved);
+ };
+ popup.on('hiding', hidingHandlerRef.current);
+ },
+ [],
+ );
+
+ return (
+
+ );
+ }
+
+ export default App;
+
+---
+
+### Restore a Draft and Add a Discard Changes Button
+
+When the edit form opens, check `localStorage` for a saved draft. If a draft exists, restore its values into the form with `form.option('formData', …)` and add a **Discard Changes** button so the user can revert to the appointment's last saved state:
+
+---
+##### jQuery
+
+
+ onAppointmentFormOpening: function (e) {
+ const form = e.form;
+ const appointmentData = e.appointmentData || {};
+ const appointmentId = appointmentData.id != null ? appointmentData.id : null;
+ // Restore draft
+ const raw = localStorage.getItem('dx-scheduler-draft-' + (appointmentId ?? 'new'));
+ if (raw) {
+ const draft = JSON.parse(raw);
+ if (draft.startDate) draft.startDate = new Date(draft.startDate);
+ if (draft.endDate) draft.endDate = new Date(draft.endDate);
+
+ form.option('formData', Object.assign({}, form.option('formData'), draft));
+
+ // Add Discard Changes button
+ const originalData = Object.assign({}, appointmentData);
+ const items = form.option('items');
+ const alreadyAdded = items.some(function (item) {
+ return item.name === 'discardChangesButton';
+ });
+
+ if (!alreadyAdded) {
+ items.push({
+ itemType: 'button',
+ name: 'discardChangesButton',
+ horizontalAlignment: 'left',
+ buttonOptions: {
+ text: 'Discard Changes',
+ type: 'danger',
+ stylingMode: 'outlined',
+ icon: 'undo',
+ onClick: function () {
+ localStorage.removeItem('dx-scheduler-draft-' + (appointmentId ?? 'new'));
+
+ form.option('formData', Object.assign({}, form.option('formData'), {
+ text: originalData.text,
+ description: originalData.description,
+ startDate: originalData.startDate,
+ endDate: originalData.endDate,
+ allDay: originalData.allDay || false,
+ }));
+
+ form.option('items', form.option('items').filter(function (item) {
+ return item.name !== 'discardChangesButton';
+ }));
+ },
+ },
+ });
+ form.option('items', items);
+ }
+ }
+
+ // Wire up cancel detection
+ // ...
+ },
+
+##### Angular
+
+
+
+
+
+ import { type DxSchedulerTypes } from 'devextreme-angular/ui/scheduler';
+ // ...
+ export class AppComponent {
+ // ...
+ handleAppointmentFormOpening(e: DxSchedulerTypes.AppointmentFormOpeningEvent): void {
+ const { form } = e;
+ const appointmentData = (e.appointmentData ?? {}) as Record;
+ const appointmentId = appointmentData['id'] != null ? appointmentData['id'] : null;
+
+ // Restore draft
+ const raw = localStorage.getItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);
+ if (raw) {
+ const draft = JSON.parse(raw) as Record;
+ if (draft['startDate']) draft['startDate'] = new Date(draft['startDate'] as string);
+ if (draft['endDate']) draft['endDate'] = new Date(draft['endDate'] as string);
+
+ form.option('formData', { ...form.option('formData') as object, ...draft });
+
+ // Add Discard Changes button
+ const originalData = { ...appointmentData };
+ const items = form.option('items') as object[];
+ const alreadyAdded = items.some((item) => (item as { name?: string })['name'] === 'discardChangesButton');
+
+ if (!alreadyAdded) {
+ items.push({
+ itemType: 'button',
+ name: 'discardChangesButton',
+ horizontalAlignment: 'left',
+ buttonOptions: {
+ text: 'Discard Changes',
+ type: 'danger',
+ stylingMode: 'outlined',
+ icon: 'undo',
+ onClick: () => {
+ localStorage.removeItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);
+
+ form.option('formData', {
+ ...form.option('formData') as object,
+ text: originalData['text'],
+ description: originalData['description'],
+ startDate: originalData['startDate'],
+ endDate: originalData['endDate'],
+ allDay: originalData['allDay'] ?? false,
+ });
+
+ form.option(
+ 'items',
+ (form.option('items') as object[]).filter(
+ (item) => (item as { name?: string })['name'] !== 'discardChangesButton',
+ ),
+ );
+ },
+ },
+ });
+ form.option('items', items);
+ }
+ }
+
+ // Wire up cancel detection
+ // ...
+ }
+ }
+
+##### Vue
+
+
+
+
+
+
+
+
+##### React
+
+
+ import { useRef, useCallback } from 'react';
+ import { Scheduler, type SchedulerTypes } from 'devextreme-react/scheduler';
+
+ function App() {
+ const isSaved = useRef(false);
+
+ const handleAppointmentFormOpening = useCallback(
+ (e: SchedulerTypes.AppointmentFormOpeningEvent) => {
+ const { form } = e;
+ const appointmentData = (e.appointmentData ?? {}) as Record;
+ const appointmentId = appointmentData['id'] != null ? appointmentData['id'] : null;
+
+ // Restore draft
+ const raw = localStorage.getItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);
+ if (raw) {
+ const draft = JSON.parse(raw) as Record;
+ if (draft['startDate']) draft['startDate'] = new Date(draft['startDate'] as string);
+ if (draft['endDate']) draft['endDate'] = new Date(draft['endDate'] as string);
+
+ form.option('formData', { ...form.option('formData') as object, ...draft });
+
+ // Add Discard Changes button
+ const originalData = { ...appointmentData };
+ const items = form.option('items') as object[];
+ const alreadyAdded = items.some(
+ (item) => (item as { name?: string })['name'] === 'discardChangesButton',
+ );
+
+ if (!alreadyAdded) {
+ items.push({
+ itemType: 'button',
+ name: 'discardChangesButton',
+ horizontalAlignment: 'left',
+ buttonOptions: {
+ text: 'Discard Changes',
+ type: 'danger',
+ stylingMode: 'outlined',
+ icon: 'undo',
+ onClick: () => {
+ localStorage.removeItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);
+
+ form.option('formData', {
+ ...form.option('formData') as object,
+ text: originalData['text'],
+ description: originalData['description'],
+ startDate: originalData['startDate'],
+ endDate: originalData['endDate'],
+ allDay: originalData['allDay'] ?? false,
+ });
+
+ form.option(
+ 'items',
+ (form.option('items') as object[]).filter(
+ (item) => (item as { name?: string })['name'] !== 'discardChangesButton',
+ ),
+ );
+ },
+ },
+ });
+ form.option('items', items);
+ }
+ }
+
+ // Wire up cancel detection
+ // ...
+ },
+ [],
+ );
+
+ return (
+
+ );
+ }
+ export default App;
+
+---
+
+### Clear the Draft After a Successful Save
+
+Clear the draft from `localStorage` after the user saves the appointment to avoid restoring stale data on subsequent opens:
+
+---
+
+##### jQuery
+
+
+ $('#scheduler').dxScheduler({
+ // ...
+ onAppointmentAdding: function () {
+ isSaved = true;
+ localStorage.removeItem('dx-scheduler-draft-new');
+ },
+ onAppointmentUpdating: function (e) {
+ isSaved = true;
+ const appointmentId = e.oldData && e.oldData.id != null ? e.oldData.id : null;
+ localStorage.removeItem('dx-scheduler-draft-' + appointmentId);
+ },
+ });
+
+##### Angular
+
+
+
+
+
+ import { type DxSchedulerTypes } from 'devextreme-angular/ui/scheduler';
+ // ...
+ export class AppComponent {
+ // ...
+ handleAppointmentAdding(): void {
+ this.isSaved = true;
+ localStorage.removeItem('dx-scheduler-draft-new');
+ }
+
+ handleAppointmentUpdating(e: DxSchedulerTypes.AppointmentUpdatingEvent): void {
+ this.isSaved = true;
+ const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
+ localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);
+ }
+ }
+
+##### Vue
+
+
+
+
+
+
+
+
+##### React
+
+
+ import { useRef, useCallback } from 'react';
+ import { Scheduler, type SchedulerTypes } from 'devextreme-react/scheduler';
+
+ function App() {
+ const isSaved = useRef(false);
+
+ const handleAppointmentAdding = useCallback(() => {
+ isSaved.current = true;
+ localStorage.removeItem('dx-scheduler-draft-new');
+ }, []);
+
+ const handleAppointmentUpdating = useCallback(
+ (e: SchedulerTypes.AppointmentUpdatingEvent) => {
+ isSaved.current = true;
+ const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
+ localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);
+ },
+ [],
+ );
+
+ return (
+
+ );
+ }
+
+ export default App;
+
+---
diff --git a/concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/25 Recreate the Legacy Form Layout.md b/concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/25 Recreate the Legacy Form Layout.md
new file mode 100644
index 0000000000..a3a4cd61ac
--- /dev/null
+++ b/concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/25 Recreate the Legacy Form Layout.md
@@ -0,0 +1,624 @@
+In DevExtreme v25.2, the appointment edit form was updated with a new layout. If your application depends on the previous form layout, you can replicate it using [editing.form.items](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/editing/form/items.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/editing/form/#items') and [onAppointmentFormOpening](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/onAppointmentFormOpening.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#onAppointmentFormOpening').
+
+
+
+### Define the Legacy Form Layout
+
+Use **editing**.**form**.[items[]](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/editing/form/items.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/editing/form/#items') to rearrange predefined groups and inject a custom repeat toggle. Both `mainGroup` and `recurrenceGroup` must stay at the root level.
+
+Two details are critical for the recurrence panel to work correctly:
+
+- Set `cssClass: ''` on both `mainGroup` and `recurrenceGroup`. The Scheduler internally uses the `dx-scheduler-form-recurrence-group-hidden` CSS class to control `recurrenceGroup` visibility. Setting `cssClass: ''` prevents that class from being applied and lets you control visibility manually.
+- Set `visible: false` on `recurrenceGroup` so the panel starts hidden. List its children explicitly so the Scheduler initializes the recurrence editors on form build:
+
+---
+
+##### jQuery
+
+
+ const legacyFormItems = [
+ {
+ name: 'mainGroup',
+ cssClass: '',
+ items: [
+ 'subjectGroup',
+ 'dateGroup',
+ {
+ name: 'repeatGroup',
+ items: [
+ 'repeatIcon',
+ {
+ name: 'customRepeatEditor',
+ editorType: 'dxSwitch',
+ dataField: 'repeat',
+ label: { text: 'Repeat' },
+ editorOptions: {},
+ },
+ ],
+ },
+ 'resourcesGroup',
+ 'descriptionGroup',
+ ],
+ },
+ {
+ name: 'recurrenceGroup',
+ itemType: 'group',
+ cssClass: '',
+ visible: false,
+ items: [
+ 'recurrenceRuleGroup',
+ 'recurrenceEndGroup',
+ ],
+ },
+ ];
+
+ $('#scheduler').dxScheduler({
+ editing: {
+ form: {
+ iconsShowMode: 'none',
+ items: legacyFormItems,
+ },
+ },
+ });
+
+##### Angular
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+##### Vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+##### React
+
+
+ import { Scheduler, Editing, Form } from 'devextreme-react/scheduler';
+
+ const legacyFormItems = [
+ {
+ name: 'mainGroup',
+ cssClass: '',
+ items: [
+ 'subjectGroup',
+ 'dateGroup',
+ {
+ name: 'repeatGroup',
+ items: [
+ 'repeatIcon',
+ {
+ name: 'customRepeatEditor',
+ editorType: 'dxSwitch',
+ dataField: 'repeat',
+ label: { text: 'Repeat' },
+ editorOptions: {},
+ },
+ ],
+ },
+ 'resourcesGroup',
+ 'descriptionGroup',
+ ],
+ },
+ {
+ name: 'recurrenceGroup',
+ itemType: 'group' as const,
+ cssClass: '',
+ visible: false,
+ items: [
+ 'recurrenceRuleGroup',
+ 'recurrenceEndGroup',
+ ],
+ },
+ ];
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ export default App;
+
+---
+
+Set [iconsShowMode](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/editing/form/iconsShowMode.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/editing/form/#iconsShowMode') to `'none'` to hide the group prefix icons that appear in the default v25.2 layout.
+
+Refer to [Predefined Items](/concepts/05%20UI%20Components/Scheduler/030%20Appointments/070%20Appointment%20Edit%20Form/15%20Predefined%20Items.md '/Documentation/Guide/UI_Components/Scheduler/Appointments/Appointment_Edit_Form/#Predefined_Items') for the full list of available group and item names.
+
+### Customize the Popup Toolbar
+
+Use [editing.popup](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/editing/popup.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#editing') to move the action buttons to the top toolbar and add a title label, matching the legacy popup appearance:
+
+---
+
+##### jQuery
+
+
+ $('#scheduler').dxScheduler({
+ editing: {
+ popup: {
+ maxWidth: 800,
+ toolbarItems: [
+ {
+ toolbar: 'top',
+ location: 'before',
+ text: 'Edit Appointment',
+ cssClass: 'dx-toolbar-label',
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'done',
+ options: {
+ stylingMode: 'contained',
+ type: 'default',
+ text: 'Save',
+ },
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'cancel',
+ },
+ ],
+ },
+ },
+ });
+
+##### Angular
+
+
+
+
+
+
+
+
+
+ import { Component } from '@angular/core';
+ import { DxSchedulerModule } from 'devextreme-angular/ui/scheduler';
+
+ @Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ standalone: true,
+ imports: [DxSchedulerModule],
+ })
+ export class AppComponent {
+ popupToolbarItems = [
+ {
+ toolbar: 'top',
+ location: 'before',
+ text: 'Edit Appointment',
+ cssClass: 'dx-toolbar-label',
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'done',
+ options: {
+ stylingMode: 'contained',
+ type: 'default',
+ text: 'Save',
+ },
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'cancel',
+ },
+ ];
+ }
+
+##### Vue
+
+
+
+
+
+
+
+
+
+
+
+
+##### React
+
+
+ import { Scheduler, Editing, Popup } from 'devextreme-react/scheduler';
+
+ const popupToolbarItems = [
+ {
+ toolbar: 'top',
+ location: 'before',
+ text: 'Edit Appointment',
+ cssClass: 'dx-toolbar-label',
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'done',
+ options: {
+ stylingMode: 'contained',
+ type: 'default',
+ text: 'Save',
+ },
+ },
+ {
+ toolbar: 'top',
+ location: 'after',
+ shortcut: 'cancel',
+ },
+ ];
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ export default App;
+
+---
+
+### Wire Up the Repeat Toggle
+
+The built-in recurrence panel switch fires when the user changes the **Repeat** drop-down in the default form. You must wire up the toggle logic manually in `onAppointmentFormOpening` because the legacy form replaces that drop-down with a custom `dxSwitch`.
+
+The `applyRepeatState` helper toggles the layout between one and two columns, displays or hides `recurrenceGroup`, and sets or clears `formData.recurrenceRule`. All three operations must happen together.
+
+`cssClass: ''` on `recurrenceGroup` (set in the previous section) is what makes this possible: it prevents the Scheduler from applying its internal `dx-scheduler-form-recurrence-group-hidden` class, so `form.itemOption('recurrenceGroup', 'visible', ...)` has full control over the panel.
+
+The `fieldDataChanged` listener keeps the toggle in sync when the user edits the recurrence rule from inside the panel (which can also clear the rule):
+
+---
+
+##### jQuery
+
+
+ function applyRepeatState(form, isRepeat) {
+ if (isRepeat) {
+ const recurrenceRule = form.option('formData') && form.option('formData').recurrenceRule;
+ form.option('colCount', 2);
+ form.option('formData.recurrenceRule', recurrenceRule || 'FREQ=DAILY');
+ form.itemOption('recurrenceGroup', 'visible', true);
+ } else {
+ form.option('colCount', 1);
+ form.option('formData.recurrenceRule', '');
+ form.itemOption('recurrenceGroup', 'visible', false);
+ }
+ }
+
+ $('#scheduler').dxScheduler({
+ onAppointmentFormOpening: function (e) {
+ const form = e.form;
+
+ form.on('fieldDataChanged', function (fe) {
+ if (fe.dataField === 'recurrenceRule') {
+ form.option('formData.repeat', !!fe.value);
+ }
+ });
+
+ const hasRecurrence = !!(e.appointmentData && e.appointmentData.recurrenceRule);
+ form.option('formData.repeat', hasRecurrence);
+ applyRepeatState(form, hasRecurrence);
+
+ form.itemOption('mainGroup.repeatGroup.customRepeatEditor', 'editorOptions', {
+ onValueChanged: function (ve) {
+ applyRepeatState(form, ve.value);
+ },
+ });
+ },
+
+ onAppointmentAdding: function (e) {
+ delete e.appointmentData.repeat;
+ },
+ onAppointmentUpdating: function (e) {
+ delete e.newData.repeat;
+ },
+ });
+
+##### Angular
+
+
+
+
+
+ import { Component } from '@angular/core';
+ import { DxSchedulerModule, type DxSchedulerTypes } from 'devextreme-angular/ui/scheduler';
+
+ @Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ standalone: true,
+ imports: [DxSchedulerModule],
+ })
+ export class AppComponent {
+ private applyRepeatState(
+ form: DxSchedulerTypes.AppointmentFormOpeningEvent['form'],
+ isRepeat: boolean,
+ ): void {
+ if (isRepeat) {
+ const recurrenceRule = (form.option('formData') as Record)?.['recurrenceRule'] as string;
+ form.option('colCount', 2);
+ form.option('formData.recurrenceRule', recurrenceRule || 'FREQ=DAILY');
+ form.itemOption('recurrenceGroup', 'visible', true);
+ } else {
+ form.option('colCount', 1);
+ form.option('formData.recurrenceRule', '');
+ form.itemOption('recurrenceGroup', 'visible', false);
+ }
+ }
+
+ handleAppointmentFormOpening(e: DxSchedulerTypes.AppointmentFormOpeningEvent): void {
+ const { form } = e;
+
+ form.on('fieldDataChanged', (fe: { dataField: string; value: unknown }) => {
+ if (fe.dataField === 'recurrenceRule') {
+ form.option('formData.repeat', !!fe.value);
+ }
+ });
+
+ const hasRecurrence = !!(e.appointmentData && e.appointmentData['recurrenceRule']);
+ form.option('formData.repeat', hasRecurrence);
+ this.applyRepeatState(form, hasRecurrence);
+
+ form.itemOption('mainGroup.repeatGroup.customRepeatEditor', 'editorOptions', {
+ onValueChanged: (ve: { value: boolean }) => {
+ this.applyRepeatState(form, ve.value);
+ },
+ });
+ }
+
+ handleAppointmentAdding(e: DxSchedulerTypes.AppointmentAddingEvent): void {
+ delete (e.appointmentData as Record)['repeat'];
+ }
+
+ handleAppointmentUpdating(e: DxSchedulerTypes.AppointmentUpdatingEvent): void {
+ delete (e.newData as Record)['repeat'];
+ }
+ }
+
+##### Vue
+
+
+
+
+
+
+
+
+##### React
+
+
+ import { useCallback } from 'react';
+ import { Scheduler, type SchedulerTypes } from 'devextreme-react/scheduler';
+
+ function applyRepeatState(
+ form: SchedulerTypes.AppointmentFormOpeningEvent['form'],
+ isRepeat: boolean,
+ ): void {
+ if (isRepeat) {
+ const recurrenceRule = (form.option('formData') as Record)?.['recurrenceRule'] as string;
+ form.option('colCount', 2);
+ form.option('formData.recurrenceRule', recurrenceRule || 'FREQ=DAILY');
+ form.itemOption('recurrenceGroup', 'visible', true);
+ } else {
+ form.option('colCount', 1);
+ form.option('formData.recurrenceRule', '');
+ form.itemOption('recurrenceGroup', 'visible', false);
+ }
+ }
+
+ function App() {
+ const handleAppointmentFormOpening = useCallback(
+ (e: SchedulerTypes.AppointmentFormOpeningEvent) => {
+ const { form } = e;
+
+ form.on('fieldDataChanged', (fe: { dataField: string; value: unknown }) => {
+ if (fe.dataField === 'recurrenceRule') {
+ form.option('formData.repeat', !!fe.value);
+ }
+ });
+
+ const hasRecurrence = !!(e.appointmentData && e.appointmentData['recurrenceRule']);
+ form.option('formData.repeat', hasRecurrence);
+ applyRepeatState(form, hasRecurrence);
+
+ form.itemOption('mainGroup.repeatGroup.customRepeatEditor', 'editorOptions', {
+ onValueChanged: (ve: { value: boolean }) => {
+ applyRepeatState(form, ve.value);
+ },
+ });
+ },
+ [],
+ );
+
+ const handleAppointmentAdding = useCallback(
+ (e: SchedulerTypes.AppointmentAddingEvent) => {
+ delete (e.appointmentData as Record)['repeat'];
+ },
+ [],
+ );
+
+ const handleAppointmentUpdating = useCallback(
+ (e: SchedulerTypes.AppointmentUpdatingEvent) => {
+ delete (e.newData as Record)['repeat'];
+ },
+ [],
+ );
+
+ return (
+
+ );
+ }
+
+ export default App;
+
+---
+
+The `repeat` field is a transient UI-only value. Remove it from the appointment data in [onAppointmentAdding](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/onAppointmentAdding.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#onAppointmentAdding') and [onAppointmentUpdating](/api-reference/10%20UI%20Components/dxScheduler/1%20Configuration/onAppointmentUpdating.md '/Documentation/ApiReference/UI_Components/dxScheduler/Configuration/#onAppointmentUpdating') so it is never written to the data source.