Skip to content

Scheduler: update Appointment Edit Form how-to#8742

Open
vladaskorohodova wants to merge 6 commits into
DevExpress:25_2from
vladaskorohodova:scheduler-preserve-unsaved-changes25_2
Open

Scheduler: update Appointment Edit Form how-to#8742
vladaskorohodova wants to merge 6 commits into
DevExpress:25_2from
vladaskorohodova:scheduler-preserve-unsaved-changes25_2

Conversation

@vladaskorohodova
Copy link
Copy Markdown
Collaborator

No description provided.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Scheduler how-to topic and an accompanying embedded demo that shows how to preserve unsaved appointment edit form changes by saving drafts to localStorage, restoring them on reopen, offering a “Discard Changes” action, and clearing drafts after successful saves.

Changes:

  • Added a new documentation topic describing draft save/restore/clear flows for jQuery, Angular, Vue, and React.
  • Added a new embedded demo (HTML/JS/CSS) to illustrate the behavior in the documentation simulator.
  • Implemented a “Discard Changes” button and popup hiding handling in the demo to persist drafts.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.

File Description
concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md New how-to documentation for persisting unsaved appointment form edits via localStorage.
applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js New demo logic for saving/restoring drafts, adding a discard button, and clearing drafts on save.
applications/UIWidgets/Guides/SchedulerPreserveChanges/index.html Demo host markup for the Scheduler instance.
applications/UIWidgets/Guides/SchedulerPreserveChanges/index.css Demo styling (Scheduler height and discard button item spacing/border).

Comment thread applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js
Comment thread applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:269

  • After the user clicks Discard Changes, the draft is removed, but the cancel-detection logic (Popup hiding handler guarded only by isSaved) can still run when the popup closes and immediately save a new draft again. Consider setting a separate flag (or temporarily disabling the hiding handler) when changes are discarded so closing the form does not recreate the draft.
                        onClick: function () {
                            localStorage.removeItem('dx-scheduler-draft-' + (appointmentId ?? 'new'));

                            form.option('formData', Object.assign({}, form.option('formData'), {
                                text: originalData.text,

Comment thread applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js
Comment thread applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js Outdated
Comment thread applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js
…ub.com/vladaskorohodova/devextreme-documentation into scheduler-preserve-unsaved-changes25_2

# Conflicts:
#	concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 25 comments.

Comments suppressed due to low confidence (16)

applications/UIWidgets/Guides/SchedulerPreserveChanges/index.js:219

  • onAppointmentUpdating runs before the appointment is updated and the operation can still be canceled or fail. Clearing the draft and marking the form as saved here can lose the only copy of the user's changes; clear the draft only after a successful update.
        onAppointmentUpdating: function (e) {
            isSaved = true;
            const appointmentId = e && e.oldData && e.oldData.id != null ? e.oldData.id : null;
            clearDraft(appointmentId);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:572

  • onAppointmentAdding fires before the appointment is added and can still be canceled, so this does not satisfy the section's "after a successful save" requirement. Clearing the draft here can lose unsaved changes if the add is canceled or fails.
        handleAppointmentAdding(): void {
            this.isSaved = true;
            localStorage.removeItem('dx-scheduler-draft-new');

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:578

  • onAppointmentUpdating fires before the appointment is updated and can still be canceled, so this does not satisfy the section's "after a successful save" requirement. Clearing the draft here can lose unsaved changes if the update is canceled or fails.
        handleAppointmentUpdating(e: DxSchedulerTypes.AppointmentUpdatingEvent): void {
            this.isSaved = true;
            const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
            localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:600

  • onAppointmentAdding fires before the appointment is added and can still be canceled, so this does not satisfy the section's "after a successful save" requirement. Clearing the draft here can lose unsaved changes if the add is canceled or fails.
    function handleAppointmentAdding(): void {
        isSaved.value = true;
        localStorage.removeItem('dx-scheduler-draft-new');

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:606

  • onAppointmentUpdating fires before the appointment is updated and can still be canceled, so this does not satisfy the section's "after a successful save" requirement. Clearing the draft here can lose unsaved changes if the update is canceled or fails.
    function handleAppointmentUpdating(e: DxSchedulerTypes.AppointmentUpdatingEvent): void {
        isSaved.value = true;
        const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
        localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:621

  • onAppointmentAdding fires before the appointment is added and can still be canceled, so this does not satisfy the section's "after a successful save" requirement. Clearing the draft here can lose unsaved changes if the add is canceled or fails.
        const handleAppointmentAdding = useCallback(() => {
            isSaved.current = true;
            localStorage.removeItem('dx-scheduler-draft-new');

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:628

  • onAppointmentUpdating fires before the appointment is updated and can still be canceled, so this does not satisfy the section's "after a successful save" requirement. Clearing the draft here can lose unsaved changes if the update is canceled or fails.
        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}`);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:578

  • This removal uses the literal null key when oldData.id is missing, but drafts are saved and restored with the fallback key dx-scheduler-draft-new. For appointments without an id, the stale draft will remain and be restored again.
            const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
            localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:606

  • This removal uses the literal null key when oldData.id is missing, but drafts are saved and restored with the fallback key dx-scheduler-draft-new. For appointments without an id, the stale draft will remain and be restored again.
        const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
        localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:628

  • This removal uses the literal null key when oldData.id is missing, but drafts are saved and restored with the fallback key dx-scheduler-draft-new. For appointments without an id, the stale draft will remain and be restored again.
                const appointmentId = e.oldData?.['id'] != null ? e.oldData['id'] : null;
                localStorage.removeItem(`dx-scheduler-draft-${appointmentId}`);

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:350

  • After this removes the draft, the cancel-detection handler from the previous section still has isSaved === false. If the user closes the popup after discarding, the hiding handler saves a new draft immediately, so the discarded changes reappear on the next open.
                            onClick: () => {
                                localStorage.removeItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);

                                form.option('formData', {

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:426

  • After this removes the draft, the cancel-detection handler from the previous section still has isSaved === false. If the user closes the popup after discarding, the hiding handler saves a new draft immediately, so the discarded changes reappear on the next open.
                        onClick: () => {
                            localStorage.removeItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);

                            form.option('formData', {

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/20 Preserve Unsaved Changes.md:497

  • After this removes the draft, the cancel-detection handler from the previous section still has isSaved === false. If the user closes the popup after discarding, the hiding handler saves a new draft immediately, so the discarded changes reappear on the next open.
                                onClick: () => {
                                    localStorage.removeItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);

                                    form.option('formData', {

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/25 Recreate the Legacy Form Layout.md:470

  • This subscribes a new fieldDataChanged handler every time the appointment form opens, but the Scheduler reuses the form instance. After several openings, each recurrence edit will run duplicated handlers; keep a handler reference and detach/replace it before subscribing again.
            form.on('fieldDataChanged', (fe: { dataField: string; value: unknown }) => {
                if (fe.dataField === 'recurrenceRule') {
                    form.option('formData.repeat', !!fe.value);
                }
            });

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/25 Recreate the Legacy Form Layout.md:529

  • This subscribes a new fieldDataChanged handler every time the appointment form opens, but the Scheduler reuses the form instance. After several openings, each recurrence edit will run duplicated handlers; keep a handler reference and detach/replace it before subscribing again.
        form.on('fieldDataChanged', (fe: { dataField: string; value: unknown }) => {
            if (fe.dataField === 'recurrenceRule') {
                form.option('formData.repeat', !!fe.value);
            }
        });

concepts/05 UI Components/Scheduler/030 Appointments/070 Appointment Edit Form/25 Recreate the Legacy Form Layout.md:582

  • This subscribes a new fieldDataChanged handler every time the appointment form opens, but the Scheduler reuses the form instance. After several openings, each recurrence edit will run duplicated handlers; keep a handler reference and detach/replace it before subscribing again.
                form.on('fieldDataChanged', (fe: { dataField: string; value: unknown }) => {
                    if (fe.dataField === 'recurrenceRule') {
                        form.option('formData.repeat', !!fe.value);
                    }
                });

Comment on lines +45 to +68
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;
Comment on lines +23 to +27
const draft = {
text: formData.text,
description: formData.description,
startDate: formData.startDate instanceof Date
? formData.startDate.toISOString()
Comment on lines +84 to +88
const draft = {
text: formData['text'],
description: formData['description'],
startDate: formData['startDate'] instanceof Date
? (formData['startDate'] as Date).toISOString()
Comment on lines +142 to +146
const draft = {
text: formData['text'],
description: formData['description'],
startDate: formData['startDate'] instanceof Date
? (formData['startDate'] as Date).toISOString()
Comment on lines +193 to +197
const draft = {
text: formData['text'],
description: formData['description'],
startDate: formData['startDate'] instanceof Date
? (formData['startDate'] as Date).toISOString()
Comment on lines +469 to +515
const raw = localStorage.getItem(`dx-scheduler-draft-${appointmentId ?? 'new'}`);
if (raw) {
const draft = JSON.parse(raw) as Record<string, unknown>;
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);

isSaved = false;

popup.off('hiding', hidingHandler);

### 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:
Comment on lines +201 to +206
form.on('fieldDataChanged', function (fe) {
if (fe.dataField === 'recurrenceRule') {
form.option('formData.repeat', !!fe.value);
}
});

Comment on lines +402 to +407
form.on('fieldDataChanged', function (fe) {
if (fe.dataField === 'recurrenceRule') {
form.option('formData.repeat', !!fe.value);
}
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants