Skip to main content

Editing events

This guide covers mounting the event editor next to the calendar, choosing which fields users can change, and controlling where the form appears. 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 @svar-ui/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() - a title plus a combined schedule block for start, end, and all-day. Replace or extend that list when you need extra inputs.

The field model, item contract, validation, and toolbar options all come from the underlying editor - see the SVAR Svelte Editor properties for the full contract. This guide covers only the calendar-specific wiring on top of it.

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 "@svar-ui/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 two-item config:

KeyComponentOwned fieldsPurpose
texttexttextEvent title
scheduleevent-datesstart, end, allDayCombined start, end, and all-day controls

schedule is one multi-field item, not three separate ones. It writes start, end, and allDay straight onto the event and validates that end is never earlier than start. Toggling all-day hides both time pickers while the date inputs stay visible. The event-dates and date-time-picker components are registered automatically inside the wrapper, so you don't pre-register them.

Recurring events

With recurring={true} on the calendar, getEditorItems() swaps the schedule item for an event-recurrence form whenever the selection is a series or a "this and following" edit. That form layers the RRULE controls on top of the start, end, and all-day fields; single-occurrence edits fall back to the plain event-dates schedule. See the recurring events guide for how series, single, and following edits flow through the store.

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 "@svar-ui/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 "@svar-ui/svelte-calendar";
import { Comments } from "@svar-ui/svelte-comments";
import { Tasklist } from "@svar-ui/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. The full item contract - key, comp, validation, column, multi-field keys, and the rest - lives in the SVAR Svelte Editor properties.

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

Placement and column layout

The placement prop picks where the form appears:

  • "sidebar" - the editor docks next to the calendar.
  • "fullscreen" - the editor fills the calendar area.
  • "modal" - the editor opens in a centered modal dialog.

Leave placement unset and the editor chooses a responsive default: "sidebar" on a normal-width calendar, "fullscreen" once the calendar root drops below 480px. An explicit placement always wins over that default. The full placement contract lives in the SVAR Svelte Editor properties.

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 "@svar-ui/svelte-calendar";
import type { CalendarInstanceApi } from "@svar-ui/svelte-calendar";
import { Editor, registerEditorItem } from "@svar-ui/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.