Event changes
Proposals, changes, results, policies, placement helpers, and the pointer layer.
Everything on this page is exported from @moltenship/calendar. Interactions
explains how the pieces fit together.
Proposals
actions.proposeEventChange(proposal) takes one of these:
type CalendarEventProposal =
| { cause: "create"; proposed: Placement }
| {
cause: "move" | "resize-start" | "resize-end" | "update";
eventId: EventId;
proposed: Placement;
}
| { cause: "delete"; eventId: EventId };A placement is a range (kind, start, end) plus optional
resourceIds. Leaving out resourceIds keeps the event's current resources.
Changes
onEventChange and policies receive a CalendarEventChange:
| Field | create | move, resize-*, update | delete |
|---|---|---|---|
cause | "create" | the cause | "delete" |
id | a change ID | a change ID | a change ID |
eventId | null | the event's ID | the event's ID |
event | null | your record | your record |
original | null | the placement before | the placement before |
proposed | the new placement | the new placement | null |
Placements in a change always include resourceIds. original is what the user saw, including any
change still pending for the same event.
Results
proposeEventChange resolves with { status: "committed", change } or
{ status: "rejected", reason, change?, error? }. It never rejects. The
reasons are listed on the Interactions page.
Policies
type CalendarPolicy = (
change: CalendarEventChange,
context: { events: EventSnapshot; timeZone: string },
) => Decision | Promise<Decision>;
type Decision =
{ type: "accept" } | { type: "adjust"; proposed: Placement } | { type: "reject"; reason: string };context.events includes pending changes, so a policy sees the calendar as the user does. A policy
that throws rejects the change with the reason policy-error.
| Policy | Behaviour |
|---|---|
snapPolicy(increment) | Rounds timed and floating changes to a wall-clock grid in the calendar's time zone, such as { minutes: 15 }. Moves keep their length; resizes snap only the dragged edge. All-day changes pass through. The increment must divide an hour or a day evenly. |
minimumDurationPolicy(minimum) | Lengthens timed and floating changes shorter than minimum. Resizing the start moves the start earlier; anything else moves the end later. |
boundsPolicy({ start, end }) | Rejects changes that reach outside [start, end), with the reason out-of-bounds. Both bounds are optional instants. |
readOnlyPolicy() | Rejects changes to events whose adapter isReadOnly returns true, with the reason read-only. Root runs it before your policies. |
Placement helpers
These build placements for proposals, with the same wall-clock rules the built-in gestures use.
| Function | Returns |
|---|---|
eventPlacement(normalizedEvent) | The placement of an event from the snapshot |
moveEventPlacement(placement, delta, { timeZone }) | The placement moved by a wall-clock delta, such as { days: 1, minutes: -30 }. A 09:00 event moved by a week stays at 09:00 across a DST change. |
resizeEventPlacement(placement, edge, delta, { timeZone, minimumDuration }) | One edge moved, keeping at least minimumDuration (15 minutes by default; all-day events keep a day) |
createEventPlacement(anchor, focus, resourceIds) | A placement covering two ranges of the same kind, such as the first and last slot of a drag |
toAllDayPlacement(placement, { timeZone }) | The dates a placement touches, as an all-day placement |
toTimedPlacement(placement, start, duration) | A timed placement starting at a Temporal.ZonedDateTime |
For example, this moves an event to the same time on the next day:
export function useMoveToTomorrow() {
const actions = Calendar.useCalendarActions();
const { eventSnapshot } = Calendar.useCalendarMeta();
const { timeZone } = Calendar.useCalendarState();
return (eventId: string) => {
const event = eventSnapshot.get(eventId);
if (event === undefined) return;
return actions.proposeEventChange({
cause: "move",
eventId,
proposed: moveEventPlacement(eventPlacement(event), { days: 1 }, { timeZone }),
});
};
}createEventTransactions and applyEventOverlay are the engine Root uses to run proposals and
draw pending changes. You only need them to handle changes outside React.
Pointer layer
useCalendarPointer(options) returns a ref for the element that contains a view's days, and turns
drags inside it into proposals. attachCalendarPointer(element, getOptions) does the same without
React and returns a cleanup function. Both come from @moltenship/calendar-dnd, which
@moltenship/calendar re-exports.
Prop
Type
While a gesture runs, the region has data-interaction="move", "resize", or "create", and the
event being dragged has data-dragging. Preview elements have data-calendar-ghost="event" or
"range" and read --calendar-ghost-background, --calendar-ghost-radius, and
--calendar-ghost-z-index.
Last updated on