Skip to main content

Editing events

This guide covers mounting the event editor next to the calendar, choosing which fields users can change, and switching between sidebar and modal layouts. The editor handles the full lifecycle of an event: it opens on selection, saves edits through the calendar action pipeline, and clears the selection on close.

calendar with the event editor docked as a sidebar showing title, start, end, and all-day fields for a selected event

How editing works

The Editor component is a calendar-bound wrapper around @wx/svelte-editor. Three rules describe how it behaves:

  1. Selection drives visibility. The editor renders only when editorData in calendar state holds an event. Clicking an event in the grid (or calling exec("select-event", { id })) populates editorData and shows the form. Closing the editor calls select-event with id: null.
  2. The selected event is the source of truth. The wrapper binds form values to editorData directly, not to a local copy, so edits can't drift away from calendar state.
  3. Saves flow through calendar actions. autoSave is on by default, so each change dispatches update-event through the same pipeline as drag/resize. Delete dispatches delete-event. There's no separate submit step.

The editor needs the calendar instance to read editorData and dispatch actions, so it always takes an api prop captured via bind:this or init. Default fields come from getEditorItems() - text, start, end, all-day. Replace or extend that list when you need extra inputs.

One calendar-specific quirk: when you change start on a same-day timed event, the wrapper shifts end to the new date and keeps the original end time. This stops a noon-to-three meeting from turning into a 27-hour overnight event the moment you pick a new day.

Mounting the editor

The minimum setup pairs Calendar with Editor and a captured api reference.

<script lang="ts">
import { Calendar, Editor } from "@wx/svelte-calendar";
let api = $state();
</script>

<Calendar bind:this={api} />
{#if api}
<Editor {api} />
{/if}

The {#if api} guard waits for the calendar to mount before rendering the editor - without it, the editor would try to read editorData from an undefined api. Once mounted, the editor stays in the DOM and toggles its inner form based on the current selection.

Default fields

getEditorItems() returns a four-field config:

KeyComponentPurpose
texttextEvent title
startdate-time-pickerEvent start - date plus time
enddate-time-pickerEvent end - date plus time
allDaycheckboxToggles the all-day flag and hides time parts

When allDay is checked, the editor adds a wx-editor-all-day class to the root element and CSS hides the time pickers - the date inputs stay visible. The date-time-picker component is registered automatically inside the wrapper, so you don't need to pre-register it.

Adding custom fields

Two patterns extend the form: append to the defaults, or replace them entirely.

Append to defaults

To keep title, dates, and the all-day flag but tag events with extra data, spread getEditorItems() and add your own entries on top.

<script lang="ts">
import { Calendar, Editor, getEditorItems } from "@wx/svelte-calendar";

const items = [
...getEditorItems(),
{
comp: "richselect",
key: "priority",
label: "Priority",
options: [
{ id: "high", label: "High" },
{ id: "medium", label: "Medium" },
{ id: "low", label: "Low" },
],
},
];

let api = $state();
</script>

<Calendar bind:this={api} events={data} {date} />
{#if api}
<Editor {api} {items} />
{/if}

Each item maps to a form field. key is the event property the field reads and writes. comp is the component id. Everything else passes through to that component.

Replace with custom blocks

For richer editors - comments, task lists, custom date pickers - register the components with registerEditorItem first, then build a fresh items array from scratch.

<script lang="ts">
import {
Calendar,
Editor,
registerEditorItem,
} from "@wx/svelte-calendar";
import { Comments } from "@wx/svelte-comments";
import { Tasklist } from "@wx/svelte-tasklist";

registerEditorItem("comments", Comments);
registerEditorItem("tasks", Tasklist);

const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];

const items = [
{ comp: "text", key: "text", label: "Text", column: "left" },
{
comp: "date-time-picker",
key: "start",
label: "Start date",
time: true,
config: { buttons: false },
},
{
comp: "date-time-picker",
key: "end",
label: "End date",
time: true,
config: { buttons: false },
},
{
key: "comments",
comp: "comments",
label: "Comments",
users,
activeUser: 1,
column: "left",
},
{
key: "tasks",
comp: "tasks",
label: "Checklist",
},
];

let api = $state();
</script>

<Calendar bind:this={api} events={data} {date} />
{#if api}
<Editor placement="modal" layout="columns" {api} {items} />
{/if}

registerEditorItem(name, Component) exposes a Svelte component under a string id. The matching comp value in your items array tells the editor which component to render for that field. Register components before the editor mounts.

editor opened as a centered modal dialog with a two-column layout for short fields on one side and richer components on the other

The placement prop picks where the form appears:

  • "sidebar" (default) - the editor docks next to the calendar.
  • "modal" - the editor opens in a centered modal dialog.

The layout prop arranges fields inside the form:

  • "default" - vertical stack, one field per row.
  • "columns" - items with column: "left" go into a left column; the rest fill the right column.
<Editor placement="modal" layout="columns" {api} {items} />

Column layout pairs well with modal placement when the editor mixes short fields (text, dates) on one side with larger components (comments, checklists) on the other.

Readonly mode

The editor has no dedicated readonly prop. To show events without letting users change them, set readonly={true} on the calendar. That hides the toolbar add-event button and disables drag/move/create. Events stay clickable, so just leave <Editor> out of the template when you don't want any form at all:

<Calendar bind:this={api} events={data} {date} readonly={true} />

For a read-only preview on click, keep the editor mounted and swap the items for non-editable comp types - for example, label blocks instead of text inputs.

Custom editor

When you need full control over save, delete, and close - confirmation dialogs, cross-field validation, custom bottom bars - turn off autoSave and drive the actions yourself.

<script lang="ts">
import { getContext } from "svelte";
import { Calendar } from "@wx/svelte-calendar";
import type { CalendarInstanceApi } from "@wx/svelte-calendar";
import { Editor, registerEditorItem } from "@wx/svelte-editor";

const { showModal } = getContext<{
showModal: (msg: { title: string; message: string }) => Promise<unknown>;
}>("wx-helpers");

let api = $state<CalendarInstanceApi>();
const editorData = $derived.by(() =>
api ? api.getReactiveState().editorData : null
);
const selected = $derived(editorData ? $editorData : null);

const bottomBar = {
items: [
{ comp: "button", id: "delete", text: "Delete", type: "danger", onclick: handleDelete },
{ comp: "spacer" },
{ comp: "button", id: "close", text: "Cancel", type: "default" },
{ comp: "button", id: "save", text: "Done", type: "primary" },
],
};

function closeEditor() {
api?.exec("select-event", { id: null });
}

function handleSave({ values }: { values: Record<string, any> }) {
if (!api || !selected) return;
api.exec("update-event", { id: selected.id, event: values });
}

async function handleDelete() {
if (!api || !selected) return;
try {
await showModal({ title: "Delete event?", message: "This action cannot be undone." });
} catch {
return;
}
api.exec("delete-event", { id: selected.id });
closeEditor();
}

function handleAction({
item,
changes,
}: {
item: { id?: string | number };
changes: string[];
}) {
if (item.id === "close") closeEditor();
else if (item.id === "save" && changes.length === 0) closeEditor();
}
</script>

<Calendar bind:this={api} events={data} {date} />
{#if selected}
<Editor
{items}
{bottomBar}
topBar={false}
autoSave={false}
placement="modal"
layout="columns"
values={selected}
onsave={handleSave}
onaction={handleAction} />
{/if}

A few things change in this setup:

  • autoSave={false} stops per-keystroke update-event dispatches. Edits collect inside the form until the user clicks Save.
  • topBar={false} drops the default close/delete top bar. bottomBar defines a custom button row instead.
  • onsave and onaction route Save, Cancel, and Delete to your own handlers. The example wraps Delete in a confirmation modal before dispatching delete-event.
  • values={selected} binds the form to the selected event read from getReactiveState().editorData. This pattern lets you intercept the event before it reaches the form - for example, to clone it or run validation.

The select-event action with id: null is what closes the editor. It clears editorData, and the {#if selected} branch unmounts the form.