Skip to main content

Loading

This guide explains how to get events into the calendar - as a plain JSON array from any source, from an iCalendar (.ics) file, or from a spreadsheet - and how to write the current calendar state back out as .ics.

How loading works

Calendar events live in a single array passed to the events prop. The widget doesn't care where that array came from - a static list, JSON fetched from an API, or a parsed .ics payload all work the same. Replacing the array replaces what the grid shows.

To read the current state back (after the user has added, edited, or deleted events), grab the instance API and call api.getEvents().

JSON

Event shape

Every entry is a CalendarEvent:

interface CalendarEvent {
id: string | number;
start: Date;
end: Date;
allDay?: boolean;
text?: string;
css?: string;
rrule?: string;
exdates?: Date[];
masterEventId?: string | number;
originalDate?: Date;
[key: string]: any;
}
FieldTypeDescription
idstring | numberUnique identifier. Required.
startDateEvent start. Required, must be a real Date.
endDateEvent end. Required, must be a real Date.
allDaybooleanRenders the event as a bar in the multiday section instead of a box in the time grid.
textstringLabel drawn inside the event; the editor's default title field writes here.
cssstringExtra CSS class names added to the event element - see styling.
rrulestringiCalendar RRULE string, expanded when recurring is on.
exdatesDate[]Dates excluded from the recurrence.
masterEventIdstring | numberLinks a recurrence exception back to its master event.
originalDateDateOriginal occurrence date of an exception.
[key: string]anyAny other field you attach is preserved and forwarded to renderers and handlers.

Only id, start, and end are required. Custom fields are how you bind events to the rest of the UI: a group id for calendar groups, a resource id for the timeline and resources views, or anything your own eventCss and eventContent hooks read.

const events = [
{
id: 1,
start: new Date("2026-05-05T09:00"),
end: new Date("2026-05-05T10:00"),
text: "Standup",
calendarId: "work", // custom field, used by CalendarPanel
},
{
id: 2,
start: new Date("2026-05-06"),
end: new Date("2026-05-07"),
allDay: true,
text: "Offsite",
},
];

Loading a static array

Pass the array to events and pick the date the calendar opens on:

<script setup>
import { Calendar } from "@svar-ui/vue-calendar";

const events = [
{ id: 1, start: new Date("2026-05-05T09:00"), end: new Date("2026-05-05T10:00"), text: "Standup" },
];
</script>

<template>
<Calendar :events="events" :date="new Date('2026-05-05')" />
</template>

Loading from a server

JSON has no date type, so start and end arrive as strings. Convert them before handing the array to the widget - that's the only transformation required:

<script setup>
import { ref } from "vue";
import { Calendar } from "@svar-ui/vue-calendar";

const events = ref([]);
const date = ref(new Date());

fetch("/api/events")
.then(r => r.json())
.then(raw => {
events.value = raw.map(e => ({
...e,
start: new Date(e.start),
end: new Date(e.end),
}));
date.value = events.value[0]?.start ?? new Date();
});
</script>

<template>
<Calendar :events="events" :date="date" />
</template>

If your events carry exdates, revive those too - it's an array of dates:

exdates: e.exdates?.map(d => new Date(d)),

RestDataProvider does this step for you. Its getData() fetches the list and returns events with real Date instances, so the same screen without the manual mapping looks like this:

<script setup>
import { ref } from "vue";
import { Calendar, Editor, RestDataProvider } from "@svar-ui/vue-calendar";

const provider = new RestDataProvider("https://your-backend");

const api = ref();
const events = ref([]);
const date = ref(new Date());

provider.getData().then(data => {
events.value = data;
date.value = new Date(data[0].start);
});

function init(obj) {
api.value.setNext(provider);
}
</script>

<template>
<Calendar ref="api" :init="init" :events="events" :date="date" />
<Editor v-if="api" :api="api" />
</template>

The provider is optional - it earns its place when you also want user changes saved back, since setNext(provider) forwards add-event, update-event, and delete-event over REST. See Saving to Server for that side of it. If you fetch the data yourself but still want the conversion, call provider.parseDates(raw), or pass a parseDate option to the constructor when your backend uses a non-standard date format.

Clearing the calendar

Replace the array with an empty one:

events.value = [];

The widget reacts to the change and clears the grid. If you have an Editor mounted, its bound event clears too.

iCal

For iCalendar interchange, the package re-exports two helpers from @svar-ui/calendar-ical:

Both map a small, common subset of the iCal spec:

iCal fieldCalendarEvent field
UIDid (kept as a number when numeric, string otherwise)
DTSTART / DTENDstart, end (real Date instances)
DTSTART;VALUE=DATEstart plus allDay: true
DURATIONend, when the event has no DTEND
SUMMARYtext
DESCRIPTIONdescription
RRULErrule
EXDATEexdates
RECURRENCE-IDoriginalDate, linked to its series

Importing an .ics file

Let users upload a calendar file and replace the current events with its contents:

<script setup>
import { ref } from "vue";
import { Calendar, parseICal } from "@svar-ui/vue-calendar";

const events = ref([]);

function importIcal(e) {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
events.value = parseICal(ev.target.result);
};
reader.readAsText(file);
}
</script>

<template>
<input type="file" accept=".ics" :onchange="importIcal" />
<Calendar :events="events" :date="new Date()" />
</template>

parseICal returns a fresh array, so reassigning events is enough to refresh the view. Each event already has parsed Date instances - no extra conversion needed.

Loading from a URL instead of a file input works the same way - read the response as text:

const text = await fetch("/api/calendar.ics").then(r => r.text());
events.value = parseICal(text);

Exporting to .ics

To save the current calendar state as a downloadable file, read events from the API and pass them to serializeICal:

<script setup>
import { ref } from "vue";
import { Calendar, Editor, serializeICal } from "@svar-ui/vue-calendar";

const api = ref();

function exportIcal() {
const ics = serializeICal(api.value.getEvents());
const blob = new Blob([ics], { type: "text/calendar" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "calendar.ics";
a.click();
URL.revokeObjectURL(url);
}
</script>

<template>
<button :onclick="exportIcal">Export .ics</button>
<Calendar ref="api" :events="[]" :date="new Date()" />
<Editor v-if="api" :api="api" />
</template>

api.getEvents() returns the live event list, including anything the user just added or edited. Wrap the serialized string in a Blob and trigger a download via a temporary anchor. To upload it instead, POST the same string with a text/calendar content type.

Recurring events

A repeating event travels as a master VEVENT with an RRULE, plus one VEVENT per edited occurrence sharing its UID and carrying a RECURRENCE-ID. parseICal groups them: the master gets rrule and exdates, and each override becomes an exception event linked to it. Feed the result straight to events - the calendar computes the series envelope itself, as long as recurring is on:

<Calendar :events="parseICal(text)" :recurring="true" :date="new Date()" />

serializeICal writes the pair back out, so an imported series survives a round-trip.

Two things to know:

  • Export the stored events, not a view. api.getEvents() with no arguments returns the series as stored - one master per rule. Passing a date range instead returns the expanded occurrences, and each of those is written out as a separate one-off VEVENT.
  • Rules are narrowed to what the calendar can expand. An .ics from another product may use parts of the RRULE grammar this widget does not implement; those are dropped on import, and the stored rule is the reduced one. See Recurring Events for the supported subset.

What's not covered by the iCal helpers

  • Timezones (TZID) - a TZID parameter is ignored and the value is read as the wall clock it is written in; VTIMEZONE blocks are skipped. UTC values (with a trailing Z) and floating values are supported.
  • RDATE - extra dates added to a series are ignored.
  • Attendees, alarms, attachments - not parsed, not emitted.

For any of these, treat the .ics round-trip as lossy and keep the canonical event data on your backend in JSON.

Excel

Spreadsheets are read by a separate widget - SVAR Excel Import from @svar-ui/vue-excel-import. It renders a modal wizard that walks the user through picking a file, matching its columns to the fields you declare, and confirming the result. It hands you back plain row objects; turning those into events is your step.

PRO

Excel Import is a separate PRO widget and ships in its own package.

Writing a spreadsheet out is covered in Export.

Wiring the wizard

Mount <ExcelImport> only while the dialog should be open - the component is the modal:

<script setup>
import { ref } from "vue";
import { Calendar, Editor } from "@svar-ui/vue-calendar";
import { ExcelImport } from "@svar-ui/vue-excel-import";

const events = ref([]);
const importOpen = ref(false);

const fields = [
{ id: "text", label: "Title", required: true },
{ id: "start", label: "Start", expectedType: "date", required: true },
{ id: "end", label: "End", expectedType: "date" },
];

function onimport(rows) {
events.value = rows
.filter(r => r.start instanceof Date)
.map(r => ({
...r,
end: r.end || new Date(r.start.getTime() + 60 * 60 * 1000),
}));
importOpen.value = false;
}
</script>

<template>
<button :onclick="() => (importOpen = true)">Import from Excel</button>
<Calendar :events="events" :date="new Date()" />

<ExcelImport
v-if="importOpen"
:fields="fields"
:onimport="onimport"
:generateIds="true"
:onclose="() => (importOpen = false)"
/>
</template>

Declaring fields

fields is the target shape - one entry per value you want out of the sheet. The wizard shows these as the columns to match against, and the id becomes the key on every returned row, so use the event field names you need:

OptionPurpose
idKey on the returned row. Required.
labelCaption shown in the matching step. Required.
expectedType"text", "number", "date", "boolean", or "mixed" - drives parsing and validation.
dateFormatFormat to read date cells with, when the sheet uses a non-standard one.
dateUTCReads date cells as UTC instead of local time.
requiredBlocks the import until the user matches a column to this field.
keywordsExtra header names to look for during automatic matching.

Custom fields work the same way - add { id: "calendarId", label: "Calendar" } and the value lands on each event, ready for calendar groups or your own styling hooks.

Turning rows into events

onimport(rows, result) fires when the user finishes the wizard. rows are plain objects keyed by field id, not finished events, so three things are worth handling:

  • Ids. Pass generateIds={true} and the widget fills id for you; otherwise assign one yourself, since every event needs a unique id.
  • Unparsed dates. A cell the wizard couldn't read as a date comes back as a raw value, so filter on r.start instanceof Date before trusting the row.
  • Missing end. The calendar requires both ends; default it to a fixed duration from start when the column was optional or empty.

The second argument reports how it went - { imported, skipped, errors }, where errors lists { row, reason } - which is what you show in a toast or a summary line.

Wizard options

OptionPurpose
acceptFile types the picker offers.
dataSkips the upload step and uses an already-parsed workbook.
headerRowTreats the first row as headers.
autoDetectionPre-matches columns to fields by header name and type.
previewRowCountNumber of rows shown in the preview.
selectorsWhere the column pickers sit: "top", "right", or "both".
validateYour own check over the matched columns; returns errors and warnings to show.
autoCloseCloses the modal once the import completes.
generateIdsGenerates an id for every row.
oncloseCalled when the user dismisses the wizard.

See the Excel Import documentation for the full reference and the wizard's own customization points.