Installation
import { WrCalendarEventTemplate, WrEventCalendar } from 'ngwr/event-calendar';
// WrCalendarEventTemplate is the <ng-template wrCalendarEvent> directive in the
// chip-template example below — the selector is wrCalendarEvent, the class is
// not, and imports: [] takes the class.
@Component({ imports: [WrEventCalendar, WrCalendarEventTemplate] })
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';
import { provideWrDateFnsAdapter } from 'ngwr/date/adapters/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" />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
)
);
}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
view="week"
[slotMinutes]="15"
[dayStartHour]="9"
[dayEndHour]="14"
[events]="events()"
/>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. A chip is its own stop INSIDE that cell, which is why the two tables below are separate: the same arrow key means something different depending on which of the two has focus.
| Key | Does |
|---|---|
Previous / next day, mirrored under dir="rtl". Stops at the edges of the view. | |
One week in month view, one slotMinutes row in week and day view. Holds position at the first and last row rather than wrapping sideways. | |
| First / last day of the current week row. | |
On an empty cell, emits slotClick with the slot pointed at. On a cell holding events, focuses the first chip instead. |
| Key | Does |
|---|---|
| Back to the cell the chip lives in. | |
MOVES the event — a day sideways, and a week (month view) or one slotMinutes step (week and day view) vertically. Emits eventChange with kind: 'move': the keyboard twin of the drag, and like the drag it changes nothing until you apply it. | |
RESIZES it, moving the end alone — kind: 'resize'. A resize that would end at or before the start is refused rather than inverting the event. This exists because the pointer's resize grab area is aria-hidden and unfocusable on purpose; without it, lengthening an event would be a mouse-only operation. |
Both chip gestures need editable on the calendar, and are refused for an individual event carrying editable: false — the same pair of gates the pointer path checks, so the keyboard and the mouse allow exactly the same set of edits.
API
| Name | Description | Type | Default |
|---|---|---|---|
events | Everything to render. Never mutated — see eventChange. | readonly WrCalendarEvent[] | [] |
view | Which span is shown. Two-way bindable. | WrCalendarView | 'month' |
date | Any date inside the shown span. Two-way bindable. | Date | new Date() |
views | Which buttons the view switcher offers. Empty hides it. | readonly WrCalendarView[] | ['month', 'week', 'day'] |
editable | Allow dragging chips to move and resize them. | boolean | false |
slotMinutes | Minutes per row in the time views — also the drag snap. | number | 30 |
dayStartHour | First hour the time views show. | number | 0 |
dayEndHour | First hour they do NOT show, exclusive. | number | 24 |
maxLanes | Lanes a month cell shows before collapsing the rest into “+N more”. | number | 3 |
hideHeader | Hide the built-in header — supply your own navigation instead. | boolean | false |
(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 | — |
ariaLabel | Accessible name of the grid; overridable for a page with several. | string | null | null |
CSS variables
Custom properties ngwr/event-calendar publishes. Each default below is declared on the component's own selector, so a :root override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.
| Variable | Default | Declared on |
|---|---|---|
--wr-event-calendar-band-height | 1.25rem | .wr-event-calendar |
--wr-event-calendar-chip-gap | 0.25rem | .wr-event-calendar |
--wr-event-calendar-chip-inset | 0.125rem | .wr-event-calendar |
--wr-event-calendar-chip-radius | calc(var(--wr-border-radius-sm) / 2) | .wr-event-calendar |
--wr-event-calendar-cols | 7 | .wr-event-calendar |
--wr-event-calendar-fill | var(--wr-color-fill) | .wr-event-calendar |
--wr-event-calendar-gutter | 3.75rem | .wr-event-calendar |
--wr-event-calendar-line | var(--wr-color-outline) | .wr-event-calendar |
--wr-event-calendar-slot-height | 1.5rem | .wr-event-calendar |