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;
}
| Field | Type | Description |
|---|---|---|
id | string | number | Unique identifier. Required. |
start | Date | Event start. Required, must be a real Date. |
end | Date | Event end. Required, must be a real Date. |
allDay | boolean | Renders the event as a bar in the multiday section instead of a box in the time grid. |
text | string | Label drawn inside the event; the editor's default title field writes here. |
css | string | Extra CSS class names added to the event element - see styling. |
rrule | string | iCalendar RRULE string, expanded when recurring is on. |
exdates | Date[] | Dates excluded from the recurrence. |
masterEventId | string | number | Links a recurrence exception back to its master event. |
originalDate | Date | Original occurrence date of an exception. |
[key: string] | any | Any 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:
parseICal(text)- turns an.icsstring into aCalendarEvent[]serializeICal(events)- turns aCalendarEvent[]back into an.icsstring
Both map a small, common subset of the iCal spec:
| iCal field | CalendarEvent field |
|---|---|
UID | id (kept as a number when numeric, string otherwise) |
DTSTART / DTEND | start, end (real Date instances) |
DTSTART;VALUE=DATE | start plus allDay: true |
DURATION | end, when the event has no DTEND |
SUMMARY | text |
DESCRIPTION | description |
RRULE | rrule |
EXDATE | exdates |
RECURRENCE-ID | originalDate, 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
.icsfrom 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) - aTZIDparameter is ignored and the value is read as the wall clock it is written in;VTIMEZONEblocks are skipped. UTC values (with a trailingZ) 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.
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:
| Option | Purpose |
|---|---|
id | Key on the returned row. Required. |
label | Caption shown in the matching step. Required. |
expectedType | "text", "number", "date", "boolean", or "mixed" - drives parsing and validation. |
dateFormat | Format to read date cells with, when the sheet uses a non-standard one. |
dateUTC | Reads date cells as UTC instead of local time. |
required | Blocks the import until the user matches a column to this field. |
keywords | Extra 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 fillsidfor you; otherwise assign one yourself, since every event needs a uniqueid. - Unparsed dates. A cell the wizard couldn't read as a date comes back as a raw value, so filter on
r.start instanceof Datebefore trusting the row. - Missing
end. The calendar requires both ends; default it to a fixed duration fromstartwhen 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
| Option | Purpose |
|---|---|
accept | File types the picker offers. |
data | Skips the upload step and uses an already-parsed workbook. |
headerRow | Treats the first row as headers. |
autoDetection | Pre-matches columns to fields by header name and type. |
previewRowCount | Number of rows shown in the preview. |
selectors | Where the column pickers sit: "top", "right", or "both". |
validate | Your own check over the matched columns; returns errors and warnings to show. |
autoClose | Closes the modal once the import completes. |
generateIds | Generates an id for every row. |
onclose | Called when the user dismisses the wizard. |
See the Excel Import documentation for the full reference and the wizard's own customization points.