Skip to content

Editing and Interaction

Enable editing with the master editable flag and the specific allow* permissions your product needs.

Editing is checked in layers. The scheduler first applies editable, allowCreate, allowMove, allowResize, allowDelete, event/resource locks, date locks, and permission hooks. It then projects the next schedule and rejects the mutation when conflicts resolve to severity: 'error'. Use this to block edits outside working hours.

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
editable: true,
allowCreate: true,
allowMove: true,
allowResize: true,
allowDelete: true,
conflicts: {
enabled: true,
rules: { 'outside-availability': 'error' },
},
createEventDraft: (context) => ({
id: `event-${crypto.randomUUID()}`,
resourceId: context.resourceId,
title: 'New booking',
startDateTime: context.startDateTime,
endDateTime: context.endDateTime,
status: 'draft',
}),
validateMutation: (detail) => {
if (detail.event?.locked) {
return 'Locked events cannot be changed.';
}
},
};

To enforce working hours, define them with calendars or eventSchedulerAvailability; then block the outside-availability conflict as shown above. nonWorkingTime can make closed cells visible, but it does not by itself reject create, move, resize, or editor changes.

Conflicts are evaluated against the projected next event array before the local mutation is committed. This means move, resize, create, edit, delete, bulk, template, paste, and recurrence actions can all be stopped by the same conflict configuration. Use conflicts.policy: 'prevent' when every detected conflict should block by default, or keep policy: 'mark' and promote only selected rules such as overlap, outside-availability, or blocked-time to error.

The before-change detail includes the projected conflicts array. Use it when the UI needs custom messages or business-specific decisions on top of configured conflict rules.

grid.addEventListener('event-scheduler-before-event-change', (event) => {
const blockingConflict = event.detail.conflicts?.find((conflict) =>
conflict.severity === 'error' &&
event.detail.eventId !== null &&
conflict.eventIds.includes(event.detail.eventId)
);
if (blockingConflict) {
event.preventDefault();
}
});

For one-off business rules that are not calendar rules, use permissions.canCreate, permissions.canMutate, or validateMutation. These hooks are useful for role checks, approval states, custom capacity rules, or backend-specific constraints.

If you prefer event-based validation, listen for event-scheduler-before-event-change and call event.preventDefault(). This is the same cancelable-before-event pattern used by RevoGrid editing events. Do not use event-scheduler-event-changed for cancellation; it fires after the scheduler has already accepted and applied the change.

grid.addEventListener('event-scheduler-before-event-change', (event) => {
if (event.detail.action === 'move' && event.detail.event?.locked) {
event.preventDefault();
}
});

EventSchedulerPlugin installs the shared HistoryPlugin automatically. Every accepted create, edit, move, resize, delete, paste, duplicate, recurrence, or bulk mutation is stored as one scheduler-owned event snapshot. Undo and redo restore the complete event collection and rebuild the normal scheduler projection, including conflicts, coverage, utilization, and event segments.

const plugins = await grid.getPlugins();
const history = plugins.find((plugin) =>
plugin.constructor?.name === 'HistoryPlugin'
);
history.undo();
history.redo();

The normal History keyboard shortcuts, stack limits, persistence adapters, beforeundo, beforeredo, historychanged, and historyerror behavior remain available. Canceled or rejected scheduler mutations do not create entries, and multi-event operations remain atomic.

Scheduler rows and columns are derived projection data, so history stores event entities rather than visible grid cells. If the host persists every state transition, listen to historychanged as well as the scheduler’s final mutation events and read scheduler.getProjection()?.events after undo or redo.

Event Scheduler does not require one persistence model. Choose the data ownership flow that matches your app: upload all events as host state, save the final projected schedule, save every accepted mutation, or let remote hooks load and commit data.

Use eventSchedulerEvents to upload host-owned data into the scheduler:

grid.eventSchedulerResources = resources;
grid.eventSchedulerEvents = events;

The scheduler mutates its local accepted event list after permissions, before hooks, validateMutation, and conflict rules pass. Final events such as event-scheduler-event-created, event-scheduler-event-changed, and event-scheduler-event-deleted fire after that local state has already changed, so use them for persistence and state sync. Use before hooks or validateMutation for cancellation.

Use this flow when the user can make several local edits and your app saves the whole schedule later, for example from a toolbar Save button. Read scheduler.getProjection()?.events; do not persist scheduler rows, columns, or segments because those are render artifacts.

const plugins = await grid.getPlugins();
const scheduler = plugins.find((plugin) =>
plugin.constructor?.name === 'EventSchedulerPlugin'
);
scheduler.createEvent({
id: `event-${crypto.randomUUID()}`,
resourceId: 'room-a',
title: 'Design review',
startDateTime: '2026-06-08T09:00:00.000Z',
endDateTime: '2026-06-08T10:00:00.000Z',
});
scheduler.editEvent('booking-1', {
resourceId: 'room-b',
startDateTime: '2026-06-08T10:00:00.000Z',
endDateTime: '2026-06-08T11:00:00.000Z',
});
scheduler.deleteEvent('hold-1');
const savedEvents = scheduler.getProjection()?.events ?? [];
await api.saveSchedule({ events: savedEvents });
// Later, or after clearing the scheduler, upload the saved data back.
grid.eventSchedulerEvents = savedEvents;

Use this flow when the host app owns canonical state and wants every accepted scheduler change to update that state immediately. Each final event carries detail.events, the full next accepted event list after the mutation.

let persistedEvents = [...initialEvents];
function persistSchedulerEvents(event: CustomEvent) {
persistedEvents = event.detail.events;
grid.eventSchedulerEvents = persistedEvents;
void api.saveEvents(persistedEvents);
}
grid.addEventListener('event-scheduler-event-created', persistSchedulerEvents);
grid.addEventListener('event-scheduler-event-changed', persistSchedulerEvents);
grid.addEventListener('event-scheduler-event-deleted', persistSchedulerEvents);
// Later, upload the persisted host state into a clean scheduler.
grid.eventSchedulerEvents = persistedEvents;

The final event detail also includes action-specific context such as eventId, event, previousEvent, action, and conflicts. Use those fields for audit logs or targeted API calls, but keep detail.events as the simplest full-list state sync contract.

Use remote mode when the server owns the visible range or when loading the whole schedule up front is too expensive. Remote loaders upload visible data into the scheduler. commitMutation receives the accepted projected mutation request and can return canonical server events.

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
remote: {
enabled: true,
mode: 'remote',
loadResources: ({ signal }) =>
api.loadResources({ signal }),
loadEvents: ({ dateRange, resourceIds, signal }) =>
api.loadEvents({ dateRange, resourceIds, signal }),
validateMutation: (request) =>
api.validateScheduleChange(request),
commitMutation: async (request) => {
const result = await api.saveScheduleChange({
action: request.action,
eventId: request.eventId,
event: request.event,
previousEvent: request.previousEvent,
changes: request.changes,
events: request.events,
conflicts: request.conflicts,
});
return {
accepted: true,
events: result.events,
};
},
},
};
const plugins = await grid.getPlugins();
const scheduler = plugins.find((plugin) =>
plugin.constructor?.name === 'EventSchedulerPlugin'
);
// Reload the current visible range into a clean scheduler state.
await scheduler.refreshVisibleRange();

Remote commitMutation runs after the local mutation has been accepted. If the backend rejects the change, return { accepted: false, message } or throw; with the default rollback behavior the scheduler restores the previous event list. See Remote Data for paging, state events, errors, and refresh controls.

validateMutation is the config-level equivalent of the cancelable before-change event. It receives the same detail shape and can return false or a string to reject.

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
validateMutation: ({ action, event, changes }) => {
if (action === 'delete' && event?.status === 'completed') {
return 'Completed events cannot be deleted.';
}
if (changes?.resourceId === 'locked-room') {
return 'That room is locked.';
}
},
};

The built-in create/edit dialog supports unassigned, single-resource, and multi-resource events. It keeps the compact resourceId shape for one selected resource and writes resourceIds when two or more resources are selected:

{
id: 'shared-maintenance',
resourceIds: ['crew-a', 'crew-b'],
title: 'Shared maintenance',
startDateTime: '2026-06-08T08:00:00.000Z',
endDateTime: '2026-06-08T10:00:00.000Z',
}

Multi-resource assignment is enabled by default. Disable it when each event must belong to at most one resource:

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
allowMultiResourceAssignment: false,
};

With this option disabled, selecting another resource replaces the current selection, and the built-in editor saves the assignment through resourceId. The option only constrains the built-in editor; custom editors and host-provided resourceIds remain application-owned.

The resource selector edits the event’s inline assignment fields. If your backend owns relationships through eventSchedulerAssignments, provide a custom editor and update that assignment collection in the host application.

Set eventEditor: false to disable the built-in editor, or provide a custom editor hook when the host app owns a drawer, modal, or approval flow:

grid.eventScheduler = {
view: 'week',
weekStartDate: '2026-06-08',
eventEditor: (context) => {
openProductDrawer({
mode: context.mode,
event: context.event,
resources: context.resources,
readOnly: context.readonly,
validationMessage: context.validationMessage,
onSave: context.submit,
onDelete: context.delete,
onClose: context.close,
});
return true;
},
};

The editor helpers still route changes through permissions, conflicts, event-scheduler-before-event-change, local mutation, post-commit events, and remote mutation hooks.

Use named before hooks to validate or cancel user actions before local scheduler state changes. Return false to cancel with the default message, or return a string to cancel with a product-specific validation message.

Before hookAction
beforeEventCreateCreate from slot, editor create, template create, recurrence create
beforeEventUpdateEditor/API updates and status changes
beforeEventMoveDrag move and bulk move
beforeEventResizeDrag resize
beforeEventDeleteDelete and bulk delete
beforeEventDuplicateDuplicate selected events
beforeEventLockLock or unlock changes
beforeSlotSelectEmpty-slot selection/click before create behavior

Final callbacks fire after the scheduler has committed local state and include enough context to update app state or persist to a backend.

Final callbackAction
onEventCreateEvent created, including paste/duplicate/template-created events
onEventUpdateEvent updated or status changed
onEventMoveEvent moved
onEventResizeEvent resized
onEventDeleteEvent deleted
onEventDuplicateDuplicated event created
onEventLockEvent locked or unlocked
onEventCopyEvent copied into the scheduler clipboard
onEventPastePasted event created
onSelectionChangeEvent selection changed
onSlotSelectEmpty slot selected/clicked
contextMenu.onContextMenuActionScheduler context-menu item selected
grid.eventScheduler = {
view: 'week',
weekStartDate: '2026-06-08',
beforeEventMove: ({ event, changes }) => {
if (event?.locked) return 'Locked events cannot be moved.';
return validateMoveOnServerCache(event, changes);
},
beforeEventResize: ({ event, changes }) =>
isOutsidePolicyWindow(event, changes) ? 'Outside policy window.' : undefined,
beforeEventDelete: ({ event }) =>
event?.metadata?.protected ? 'Protected events cannot be deleted.' : undefined,
beforeSlotSelect: ({ date, startDateTime }) =>
isClosedForBooking(date, startDateTime) ? 'This slot is closed.' : undefined,
onEventMove: ({ event, previousEvent }) =>
saveEventMove({ event, previousEvent }),
onEventResize: ({ event, previousEvent }) =>
saveEventResize({ event, previousEvent }),
onEventDelete: ({ event }) =>
archiveDeletedEvent(event),
onSelectionChange: ({ eventIds }) =>
setSelectedPlannerEvents(eventIds),
contextMenu: {
onContextMenuAction: ({ itemId, target, segment, slot }) =>
trackSchedulerCommand({ itemId, target, eventId: segment?.event.id, slot }),
},
};

The lower-level event-scheduler-before-event-change, event-scheduler-before-event-select, event-scheduler-before-slot-select, event-scheduler-event-created, event-scheduler-event-changed, event-scheduler-event-deleted, and event-scheduler-event-selected DOM events remain available for integrations that prefer event listeners. Call preventDefault() on the before events to cancel the pending scheduler action. validateMutation still runs in the same mutation pipeline for shared validation logic.

Scheduler selection supports selected events, selected slots, selected time ranges, multi-select, range select, copy, paste, duplicate, and keyboard-driven commands. Use the cancelable event-scheduler-before-event-select event when the host application owns selected event state: prevent the pending selection, update application state, then pass the accepted ids back through selection.selectedEventIds.

let selectedEventIds: readonly string[] = [];
grid.addEventListener('event-scheduler-before-event-select', (event) => {
event.preventDefault();
selectedEventIds = event.detail.eventIds;
renderScheduler();
});
function renderScheduler() {
grid.eventScheduler = {
...grid.eventScheduler,
selectionMode: 'multiple',
selection: {
...grid.eventScheduler?.selection,
selectedEventIds,
selectedSlot,
selectedRange,
clipboard: true,
copyOffsetDays: 7,
onChange: ({ eventIds }) => setSelectedEventIds(eventIds),
},
onSelectionChange: ({ eventIds, events }) =>
syncInspectorSelection({ eventIds, events }),
onSlotSelect: (slot) =>
setSelectedSlot(slot),
};
}

Accepted selections still emit event-scheduler-event-selected, onSelectionChange, and selection.onChange. A prevented selection does not emit final selection callbacks; the host re-applies its accepted state through the config.

grid.eventScheduler = {
view: 'week',
weekStartDate: '2026-06-08',
beforeSlotSelect: ({ date }) =>
isClosedForBooking(date) ? 'This slot is closed.' : undefined,
};
grid.addEventListener('event-scheduler-before-slot-select', (event) => {
if (isReadOnlyResource(event.detail.resourceId)) {
event.preventDefault();
}
});

Use clipboardAdapter when copy/paste must integrate with product state, permissions, or a custom clipboard store. Paste remains disabled when there is no internal payload and clipboardAdapter.hasPayload() is false.

grid.eventScheduler = {
view: 'resourceTimeline',
weekStartDate: '2026-06-08',
clipboardAdapter: {
hasPayload: () => appClipboard.has('scheduler-events'),
read: () => appClipboard.get('scheduler-events'),
write: (payload) => appClipboard.set('scheduler-events', payload),
clear: () => appClipboard.delete('scheduler-events'),
},
onCopy: ({ eventIds }) => trackCopy(eventIds),
onPaste: ({ event }) => persistCreatedEvent(event),
onDuplicate: ({ event }) => persistCreatedEvent(event),
};

Keyboard shortcuts can be disabled globally, disabled per command, or rebound. Use Mod for Cmd on macOS and Ctrl on Windows/Linux.

grid.eventScheduler = {
view: 'week',
weekStartDate: '2026-06-08',
keyboardShortcuts: {
enabled: true,
helpPanel: true,
shortcuts: {
copy: 'Mod+c',
paste: 'Mod+v',
duplicate: 'Mod+Shift+d',
delete: false,
lock: false,
today: ['t', 'Home'],
},
},
};

Available keyboard commands are escape, help, today, search, previous, next, cyclePrevious, cycleNext, open, create, edit, lock, delete, copy, paste, duplicate, and viewWeek.