ComponentStandalone

Event calendar

Month, week and day views of a set of events — draggable, resizable, keyboard-operable.

Installation

import { WrEventCalendar } from 'ngwr/event-calendar';

@Component({ imports: [WrEventCalendar] })
export class MyComponent {}

Register a date adapter

Every date calculation and every label goes through WrDateAdapter, the same one the calendar and the date pickers use. Register one at bootstrap or the calendar has no way to name a month.

import { provideWrDateAdapter } from 'ngwr/date-adapter';
import { provideWrDateFnsAdapter } from 'ngwr/date-adapter-fns';

bootstrapApplication(App, {
  providers: [provideWrDateFnsAdapter()],
});

Views

One component covers all three views. view and date are two-way bindable, so the built-in header is a convenience rather than the only way to navigate — bind them and drive the calendar from your own toolbar, or set hideHeader.

<wr-event-calendar [events]="events()" [(view)]="view" [(date)]="anchor" />

August 2026

Sun
Mon
Tue
Wed
Thu
Fri
Sat

Events

end is exclusive: an event running 09:00–10:00 does not overlap one that starts at 10:00, and a single all-day event ends at midnight the next day. That one rule makes adjacency and overlap the same comparison in every view. color takes any intent, and editable: false pins an individual event while the rest of the calendar stays draggable.

protected readonly events = signal<readonly WrCalendarEvent[]>([
  { id: 1, title: 'Design review', start: at(0, 10), end: at(0, 11, 30), color: 'primary' },
  { id: 5, title: 'Conference', start: at(2, 0), end: at(5, 0), allDay: true, color: 'secondary' },
  { id: 7, title: 'Frozen — no deploys', start: at(6, 0), end: at(7, 0), allDay: true, editable: false },
]);

Drag to move and resize

events is an input and the calendar never writes to it. A drag emits eventChange with where the event *would* land and stops there — apply it to your own state and the new position renders on the next pass. So ignoring the output cancels the drag, an optimistic update is a single update, and a server rejection needs no rollback path inside the component. Drag a chip below, or focus one and hold Alt with the arrow keys.

<wr-event-calendar
  editable
  [events]="events()"
  [(view)]="view"
  [(date)]="anchor"
  (eventChange)="apply($event)"
/>
protected apply(change: WrCalendarEventChange): void {
  this.events.update(events =>
    events.map(event =>
      event.id === change.event.id ? { ...event, start: change.start, end: change.end } : event
    )
  );
}

Aug 2, 2026 – Aug 8, 2026

Sun2
Mon3
Tue4
Wed5
Thu6
Fri7
Sat8
08:00 AM
09:00 AM
10:00 AM
11:00 AM
12:00 PM
01:00 PM
02:00 PM
03:00 PM
04:00 PM
05:00 PM
06:00 PM

Last output: —

The time grid

dayStartHour and dayEndHour trim the visible band — a calendar that opens on 3am dead space wastes most of its height. slotMinutes sets both the row height and the drag snap, so a 15-minute grid drags in 15-minute steps.

<wr-event-calendar
  editable
  view="week"
  [dayStartHour]="8"
  [dayEndHour]="19"
  [slotMinutes]="30"
  [events]="events()"
/>

Aug 2, 2026 – Aug 8, 2026

Sun2
Mon3
Tue4
Wed5
Thu6
Fri7
Sat8
09:00 AM
10:00 AM
11:00 AM
12:00 PM
01:00 PM

Creating from empty space

Clicking anywhere that is not a chip emits slotClick with the slot the user pointed at — a whole day in month view and the all-day band, the pointed-at row otherwise. From the keyboard it is Enter on an empty cell.

<wr-event-calendar [events]="events()" (slotClick)="compose($event)" />

Custom chip contents

wrCalendarEvent replaces what a chip renders. Position, sizing and drag stay with the calendar, so a custom template cannot break the geometry.

<wr-event-calendar [events]="events()">
  <ng-template wrCalendarEvent let-event>
    <strong>{{ event.title }}</strong>
    <small>{{ event.data.room }}</small>
  </ng-template>
</wr-event-calendar>

Keyboard

The grid is a single tab stop with a roving cursor, so tabbing through a month does not mean tabbing through 42 cells.

  • Arrow keys — move by a day left and right, by a week or a slot up and down.
  • Home / End — first and last day of the row.
  • Enter — on an empty cell emits `slotClick`; on a cell holding events focuses the first chip.
  • Escape — from a chip, back to its cell.
  • Alt + arrows — on a focused chip, move the event. The keyboard equivalent of the drag, emitting the same `eventChange`.

API

NameDescriptionTypeDefault
eventsEverything to render. Never mutated — see eventChange.readonly WrCalendarEvent[][]
viewWhich span is shown. Two-way bindable.WrCalendarView'month'
dateAny date inside the shown span. Two-way bindable.Datenew Date()
viewsWhich buttons the view switcher offers. Empty hides it.readonly WrCalendarView[]['month', 'week', 'day']
editableAllow dragging chips to move and resize them.booleanfalse
slotMinutesMinutes per row in the time views — also the drag snap.number30
dayStartHourFirst hour the time views show.number0
dayEndHourFirst hour they do NOT show, exclusive.number24
maxLanesLanes a month cell shows before collapsing the rest into “+N more”.number3
hideHeaderHide the built-in header — supply your own navigation instead.booleanfalse
(eventClick)A chip was activated.WrCalendarEvent
(slotClick)Empty space was activated — the slot the user pointed at.WrCalendarSlot
(eventChange)A drag or an Alt + arrow finished. Apply it yourself; nothing moves until you do.WrCalendarEventChange
ariaLabelAccessible name of the grid; overridable for a page with several.string | nullnull