Recurring events from your calendar
Your current calendar's simple recurrence and its rrule plugin's fields, as series of Hapio's recurrence engine.
The calendar you migrate from repeats events two ways: its built-in simple recurrence (daysOfWeek,
startTime, …) and the rrule plugin (rrule, exdate, duration). The migration component reads both, as
Hapio series, when you give it a recurrence engine. The engine replaces the plugin:
npm install @hapio/calendar-recurrence'use client'
import type { EventChangeInfo, EventInput } from '@hapio/calendar-compat'
import { rrule } from '@hapio/calendar-recurrence'
import { CalendarEditing } from '@/components/hapio-calendar/calendar-editing'
import { EventCalendar } from '@/components/event-calendar'
// The engine replaces the rrule plugin: once per app, at module scope.
const recurrence = rrule()
const EVENTS: readonly EventInput[] = [
// Simple recurrence: weekdays at 09:00 in the calendar's zone.
{
id: 'standup',
title: 'Stand-up',
daysOfWeek: [1, 2, 3, 4, 5],
startTime: '09:00',
endTime: '09:15',
},
// The rrule plugin's fields: a rule with its own zone, and the dates it skips.
{
id: 'review',
title: 'Monthly review',
rrule: 'DTSTART;TZID=Europe/Stockholm:20261022T070000\nRRULE:FREQ=MONTHLY;BYMONTHDAY=22',
duration: '00:45',
exdate: ['2026-12-22T07:00:00'],
},
]
export function TeamCalendar({ onChange }: { onChange: (info: EventChangeInfo) => void }) {
return (
<CalendarEditing>
<EventCalendar
events={EVENTS}
recurrence={recurrence}
timeZone="Europe/Stockholm"
editable
// `info.recurrence` says which instances changed and how to save the series.
eventChange={onChange}
/>
</CalendarEditing>
)
}Simple recurrence
daysOfWeek (0 is Sunday; numbers or their strings, '4'), startTime, endTime,
startRecur and endRecur make a weekly series on those days (a daily one without
daysOfWeek) of wall times in the calendar's zone:
startRecuris the first day an occurrence can fall on; without it the series reaches back indefinitely.endRecuris exclusive: no occurrence on that day itself.startTimeandendTimeset the times and the length (durationwins overendTime). Without either the occurrences are all-day, unlessallDaysays otherwise; withoutendTimethey take the default duration andEventApi.endisnull.- As in the source library, the event's
startandendare ignored.
The rrule plugin's fields
rruleas a string, itsDTSTART,RRULE,EXDATEandRDATElines withTZIDandVALUE=DATE('DTSTART;TZID=Europe/Stockholm:20261022T070000\nRRULE:FREQ=MONTHLY;BYMONTHDAY=22'), or the object form (freq,interval,byweekday,bymonthday,bymonth,count,until,dtstart,tzid,wkst, …) with strings, numbers or the rrule library's constants (RRule.MO.nth(2)).- DTSTART (
dtstart, else the event'sstart) starts the series. With a zone (Z, an offset,tzidorTZID) the rule repeats in that zone; without one it repeats in the calendar's wall time; a date makes the series all-day. exdateremoves occurrences, matched on their original start; offset-less date-times are wall times where the rule repeats.durationsets each occurrence's length.- A
Dateindtstart,untilorexdateis read as the rrule library reads it: its UTC fields are the wall time. - Without
wkstthe week starts on Monday, as RFC 5545 says (the plugin's default, the calendar'sfirstDay, is not applied). allDaywins over the form of the rule's dates, as in the plugin.
Changing occurrences
Inside <CalendarEditing>, moving or resizing an occurrence changes every occurrence by
default, as the source library does (recurrenceScope: 'all'). A change the rule cannot apply to every
occurrence (a move between the all-day row and the times, say) changes that occurrence only, and
development builds say so. Set recurrenceScope to 'this', 'following', 'prompt' (the
calendar asks with its scope dialog) or a function of the change to decide otherwise.
eventDrop, eventResize and eventChange receive the occurrence after (event) and before
(oldEvent) the change, relatedEvents empty, revert() (which reverts the whole change) and
recurrence: the scope taken and Hapio's plan of how the series changes. The migration component
keeps a one-off event's change over your events until they have it, but not an instance's: save
the plan back into your events, or the instances move back after the settle timeout (15
seconds). The recurring event keeps its id, title, colors and extendedProps:
| Scope | Save |
|---|---|
'this' | overrides: plan.series.overrides (the rule and everything else stay) |
'all' | the series as the plan writes it: start, rrule (a rule line without DTSTART: start is DTSTART), exdate, rdate, overrides and duration from plan.series, without the simple recurrence fields, which would win over rrule |
'following' | the event as for 'all' from plan.truncated, and a new recurring event the same way from plan.created (with the created series' id) |
overrides and rdate are Hapio's additions to EventInput, so that every plan can be saved:
the source library has no field for one changed instance (its apps add an exdate and a one-off event
instead, which the migration component reads too).
See Recurring events for scopes, plans and server-side expansion.
Not supported
- Recurring events without an engine: their fields are listed as unsupported, and an event with
simple recurrence is skipped unless it has a
start. - Recurring background events, and recurring events that an
eventsfunction returns. exrule.
Your options in Hapio
Every option, callback, event field and payload of the calendar you migrate from, what the migration component does with it, and the Hapio option it becomes.
Differences from your calendar
What the migration component does differently from the calendar you migrate from, and what it does not support.