Recurrence
RFC 5545 RRULE recurrence via a pluggable adapter (rrule by default): a standalone editor, automatic expansion in every view, exceptions, and this / this-and-following / all edit semantics.
Recurrence editor
A standalone control that reads and writes an RRULE string through a two-way rule model. Edit the recurrence below and watch the serialized rule update live.
<!-- Two-way bound to a signal holding the RRULE string. -->
<cal-recurrence-editor [(rule)]="rule" />
<p>Current rule: <code>{{ rule() }}</code></p>Automatic expansion
An event carrying a recurrenceRule field is expanded by the views automatically across the visible range — there is no manual occurrence generation. Because the rule above is bound to the same signal, editing it re-expands this month live.
import { computed, signal } from '@angular/core';
import { CalendarEvent } from '@ascentsparksoftware/angular-calendar';
rule = signal<string>('FREQ=WEEKLY;BYDAY=MO,WE,FR');
// One master event carries `recurrenceRule`. The view expands it across the
// visible range — no manual occurrence generation. Editing `rule` re-expands.
recurringEvents = computed<CalendarEvent[]>(() => [
{
id: 'r1',
title: 'Standup',
start: { epochMs: Date.parse('2026-06-01T13:00:00Z'), zone: 'America/New_York' },
end: { epochMs: Date.parse('2026-06-01T13:30:00Z'), zone: 'America/New_York' },
status: 'scheduled',
recurrenceRule: rule(),
},
]);This / this-and-following / all
Editing one occurrence of a series has three scopes. THIS occurrence adds the occurrence date to recurrenceExceptions (via addRecurrenceException) and creates a standalone override linked back with recurrenceId. THIS AND FOLLOWING splits the series in two (splitSeriesAt terminates the master with an RRULE UNTIL and returns the rule + start for a new tail series). ALL simply edits the master's recurrenceRule so every occurrence follows. All three are pure functions over your event data.
import {
addRecurrenceException,
splitSeriesAt,
type CalendarEvent,
type ZonedDateTime,
} from '@ascentsparksoftware/angular-calendar';
// THIS occurrence: drop the occurrence from the series and create a standalone
// override that points back at the master via `recurrenceId`.
function editThis(series: CalendarEvent, occurrence: ZonedDateTime): CalendarEvent[] {
const master = addRecurrenceException(series, occurrence);
const override: CalendarEvent = {
id: crypto.randomUUID(),
title: series.title,
start: occurrence,
recurrenceId: series.id, // marks it as a detached instance of the series
};
return [master, override];
}
// THIS AND FOLLOWING: terminate the master at the occurrence (RRULE UNTIL) and
// start a fresh series from it. `ctx` supplies the recurrence + date adapters.
function editFollowing(series: CalendarEvent, occurrence: ZonedDateTime, ctx: SplitCtx) {
const { head, tailRule, tailStart } = splitSeriesAt(series, occurrence, ctx);
const tail: CalendarEvent = {
id: crypto.randomUUID(),
title: series.title,
start: tailStart,
recurrenceRule: tailRule,
};
return [head, tail]; // head keeps the past, tail carries the change forward
}
// ALL: just edit the master's recurrenceRule; every occurrence follows.
function editAll(series: CalendarEvent, nextRule: string): CalendarEvent {
return { ...series, recurrenceRule: nextRule };
}ICS export
The /export entry point serializes events to iCalendar text. A recurring event's RRULE is written straight onto its VEVENT, so importing calendars re-expand the series natively.
import { eventsToIcs } from '@ascentsparksoftware/angular-calendar/export';
import type { CalendarEvent } from '@ascentsparksoftware/angular-calendar';
const events: CalendarEvent[] = [
{
id: 'r1',
title: 'Standup',
start: { epochMs: Date.parse('2026-06-01T13:00:00Z'), zone: 'America/New_York' },
end: { epochMs: Date.parse('2026-06-01T13:30:00Z'), zone: 'America/New_York' },
recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR',
},
];
// The RRULE is preserved on the VEVENT — calendars re-expand it on import.
const ics = eventsToIcs(events);
// BEGIN:VEVENT ... RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR ... END:VEVENT