A lightweight, fully customizable Angular scheduler / calendar — all 12 view modes (Day, Week, Work Week, Month, Year, Agenda, Month Agenda, and the five Timeline views) — with zero hard runtime dependencies and a pluggable date layer (Native, Luxon, or Moment).
Status:
0.2.0pre-release. The 12 views, drag/resize, a host-driven quick-view, resources, auto-scroll, recurrence (RRULE expansion + EXDATE), a date-jump calendar popover, header/cell drill-down navigation, and virtual scrolling are implemented. A full editor window and full keyboard grid navigation are on the roadmap (see Limitations).
- Lightweight — core has no runtime deps; each view is a tree-shakeable factory; you ship only the views you import.
- Bring your own date library — Native (built in,
Intl-based), Luxon, or Moment via separate entry points. No date library baked into core. - Controlled & unopinionated editing — the library ships no form. It emits click events and shows an optional quick-view popover you can fully replace; your app owns create/edit/delete.
- Customizable — every color/size/motion value is a CSS custom property; the quick-view is
template-overridable; every fixed UI string is overridable via
provideSchedulerI18n(...). - Built-in navigation — period-aware prev / next / today and a view switcher in the header; an all-day band (collapsible) for multi-day events on Day/Week/Work Week.
- Wide Angular support — published in partial-Ivy mode, verified by real AOT builds on Angular 18–22. RTL-safe, dark-mode-ready, responsive.
npm i @datelane/core
# optional — only if you choose that date adapter:
npm i luxon # or: npm i momentPeer deps: @angular/core and @angular/common >=18.0.0 <23.0.0. luxon/moment are
optional peers.
Register the scheduler providers once at bootstrap:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideScheduler } from '@datelane/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideScheduler()], // Native date adapter by default
});Use the component and pick the views you want:
import { Component } from '@angular/core';
import {
SchedulerComponent, weekView, monthView, timelineWeekView,
type FieldMap, type ViewDescriptor, type SchedulerViewType, type SchedulerChange,
} from '@datelane/core';
@Component({
standalone: true,
selector: 'app-root',
imports: [SchedulerComponent],
template: `
<dl-scheduler
[(activeView)]="view"
[(viewDate)]="date"
[events]="events"
[fieldMap]="fieldMap"
[views]="views"
height="640px"
(eventClick)="onEventClick($event)"
(eventEdit)="openMyForm($event)"
(eventDelete)="onDelete($event)"
(eventChange)="onDragResize($event)"
(cellClick)="onEmptySlot($event)">
</dl-scheduler>`,
})
export class AppComponent {
view: SchedulerViewType = 'week';
date = new Date();
views: ViewDescriptor[] = [
weekView({ isDefault: true, startHour: '08:00', endHour: '20:00' }),
monthView(),
timelineWeekView(),
];
fieldMap: FieldMap = { id: 'id', subject: 'subject', start: 'start', end: 'end' };
events = [
{ id: 1, subject: 'Standup', start: new Date(), end: new Date(Date.now() + 30 * 60_000) },
];
onEventClick(c: SchedulerChange) { /* always fires on activation */ }
openMyForm(c: SchedulerChange) { /* quick-view "Edit" → open YOUR dialog */ }
onDelete(c: SchedulerChange) { this.events = this.events.filter(e => e.id !== c.event.id); }
onDragResize(c: SchedulerChange) { /* apply c.event.start/end back into your data */ }
onEmptySlot(p: { date: unknown; resourceId?: string | number }) { /* open create form */ }
}The package ships SCSS (tokens + theme + component styles); there is no pre-compiled CSS yet.
Import it once. Either add it to angular.json:
…or @use it from your global stylesheet:
/* src/styles.scss */
@use '@datelane/core/styles/scheduler';All 12 views are tree-shakeable factory functions returning a typed ViewDescriptor. Import only
the ones you use.
| Factory | View | Engine |
|---|---|---|
dayView() |
Day | vertical-time |
weekView() |
Week | vertical-time |
workWeekView() |
Work Week | vertical-time |
monthView() |
Month | calendar-grid |
yearView() |
Year | year-grid |
agendaView() |
Agenda | list |
monthAgendaView() |
Month Agenda | mini-calendar + list |
timelineDayView() |
Timeline Day | horizontal-time |
timelineWeekView() |
Timeline Week | horizontal-time |
timelineWorkWeekView() |
Timeline Work Week | horizontal-time |
timelineMonthView() |
Timeline Month | horizontal-time |
timelineYearView() |
Timeline Year | horizontal-time |
Each factory takes a partial ViewDescriptor to configure that view:
import { dayView, weekView, monthView, timelineYearView } from '@datelane/core';
views = [
dayView({ interval: 3, displayName: '3 Days', startHour: '08:00', endHour: '20:00' }),
weekView({ isDefault: true, firstDayOfWeek: 1, showWeekNumber: true }),
monthView({ showWeekend: false }),
timelineYearView({ orientation: 'vertical' }),
];ViewDescriptor fields: type, displayName, isDefault, interval, dateFormat, readonly,
showWeekend, showWeekNumber, workDays, firstDayOfWeek, startHour, endHour,
timeScale: { enabled, slotCount }, orientation, headerRows, allowVirtualScrolling,
grouping. Only options that apply to a given view type are honored.
You pass raw records via [events] and a [fieldMap] that maps your field names onto the canonical
event shape — no need to reshape your data.
fieldMap: FieldMap = {
id: 'Id',
subject: 'Subject',
start: 'StartTime',
end: 'EndTime',
isAllDay: 'IsAllDay', // optional
recurrenceRule: 'RecurrenceRule', // optional — RFC 5545 RRULE, expanded automatically
recurrenceExceptions: 'ExDates', // optional — EXDATE list (skipped occurrences)
resource: 'OwnerId', // optional — string or string[]
color: 'Color', // optional — overrides resource color
location: 'Location', // optional — shown in the quick-view
description: 'Description', // optional — shown in the quick-view
};Dates may be Date, ISO string, or epoch number — the active date adapter parses them.
| Input | Type | Default | Notes |
|---|---|---|---|
activeView |
SchedulerViewType |
'week' |
two-way (activeViewChange) |
viewDate |
adapter date | today | two-way (viewDateChange) |
events |
Record<string, unknown>[] |
[] |
raw records |
fieldMap |
FieldMap |
— | required to render events |
views |
ViewDescriptor[] |
[] |
from the view factories |
resources |
ResourceDefinition[] |
[] |
Timeline rows |
grouping |
GroupingConfig |
— | { resources: ['name'] } |
readonly |
boolean |
false |
disables drag/resize + quick-view actions |
rowAutoHeight |
boolean |
false |
Timeline: grow rows vs +N more |
agendaDaysCount |
number |
7 |
Agenda span |
hideEmptyAgendaDays |
boolean |
false |
Agenda |
showQuickView |
boolean |
true |
built-in popover on activation |
autoScroll |
boolean |
true |
scroll to first event (see below) |
scrollHour |
number |
— | time grids: scroll to this hour instead |
height |
string |
'600px' |
required px height for Agenda / Month Agenda |
width |
string |
'100%' |
|
dir |
'ltr' | 'rtl' |
'ltr' |
RTL mirrors the layout |
| Output | Payload | When |
|---|---|---|
eventClick |
SchedulerChange |
any event activation (always) |
eventEdit |
SchedulerChange |
quick-view Edit → open your form |
eventDelete |
SchedulerChange |
quick-view Delete |
eventChange |
SchedulerChange |
after a drag-move or resize |
eventCreate |
SchedulerChange |
reserved for the editor (pending) |
cellClick |
{ date, resourceId? } |
empty slot/cell — open your create form |
navigate |
NavigateEvent |
date navigation / drill-through |
viewChange |
SchedulerViewType |
active view changed |
SchedulerChange = { event: SchedulerEvent; scope?: 'occurrence' | 'following' | 'series' }.
The component never mutates your data — apply changes yourself and pass them back via [events].
The library intentionally ships no editor. On event activation it:
- always emits
(eventClick), and - (unless
[showQuickView]="false") opens a small built-in quick-view popover.
The quick-view's Edit / Delete buttons forward to (eventEdit) / (eventDelete) so your
app opens its own dialog. Replace the popover entirely with the ngsQuickViewTemplate directive:
<dl-scheduler …>
<ng-template ngsQuickViewTemplate let-event let-close="close" let-edit="edit" let-delete="delete">
<h4>{{ event.subject }}</h4>
<button (click)="edit()">Open my form</button>
<button (click)="delete()">Delete</button>
<button (click)="close()">Close</button>
</ng-template>
</dl-scheduler>For empty-slot creation, handle (cellClick) and open your own create dialog.
resources: ResourceDefinition[] = [{
field: 'ownerId', name: 'owners', title: 'Owner',
idField: 'id', textField: 'text', colorField: 'color',
dataSource: [
{ id: 1, text: 'Alex', color: '#2563eb' },
{ id: 2, text: 'Priya', color: '#16a34a' },
],
}];<dl-scheduler [resources]="resources" [grouping]="{ resources: ['owners'] }" …>Events are placed on the row whose idField matches the event's mapped resource. Single-level
grouping is supported today; hierarchical grouping is on the roadmap.
Scrolling views (Day/Week/Work Week, Timeline, Agenda, Year) scroll to the first event on first
render and when you navigate to a new period — but not on edits or drag, so a user's manual
scroll is preserved. Configure with [autoScroll] (default true) and, for time grids,
[scrollHour] to jump to a fixed hour instead of the first event.
Core ships the zero-dependency NativeDateAdapter. Opt into Luxon or Moment via secondary entry
points (their libraries are optional peers):
import { provideScheduler } from '@datelane/core';
import { provideLuxonDateAdapter } from '@datelane/core/luxon-adapter';
// or: import { provideMomentDateAdapter } from '@datelane/core/moment-adapter';
providers: [provideScheduler(provideLuxonDateAdapter({ locale: 'fr' }))];Dates and numbers are localized through the date adapter's locale. The fixed UI strings
(header Today / prev / next labels, the view switcher, quick-view Edit / Delete / Close,
the all-day label, and the +N more / Show less overflow text) come from an injectable message
token. Override any subset — the rest fall back to the built-in English defaults:
import { provideScheduler } from '@datelane/core';
import { provideSchedulerI18n } from '@datelane/core';
providers: [
provideScheduler(),
provideSchedulerI18n({
today: "Aujourd’hui",
previous: 'Précédent',
next: 'Suivant',
edit: 'Modifier',
delete: 'Supprimer',
moreEvents: (n) => `+${n} de plus`,
}),
];The full contract is the SchedulerMessages interface (token: SCHEDULER_MESSAGES).
The header renders Today · ‹ · › plus a view switcher (shown when you pass two or more
[views]). Prev / next step by the active view's own unit — a day (or interval days), a week, a
month, a year, or the agenda's day count — and emit (navigate) with action: 'prev' | 'next' | 'today'. Switching a view emits (viewChange). Both drive the two-way [(viewDate)] /
[(activeView)], so the host stays the source of truth.
Multi-day and all-day events render as spanning bars in an all-day band above the time grid.
Overlapping events stack into lanes; when there are more lanes than the collapsed cap the extra
ones fold behind a +N more toggle. Configure the cap on the view via [allDayMaxLanes]
(default 2).
The components are standalone-only (no NgModule surface). Apps still organised around
NgModules import the standalone components directly — Angular has supported importing
standalone components into an NgModule since v14:
import { SchedulerComponent } from '@datelane/core';
@NgModule({ imports: [SchedulerComponent] })
export class AppModule {}Override any token on .dl-scheduler (or an ancestor) — no ::ng-deep, no fork:
.dl-scheduler {
--dl-accent: #2563eb;
--dl-radius-md: 10px;
--dl-slot-h: 48px;
}
.dl-scheduler[data-dl-theme="dark"] { /* built-in dark theme */ }Full token list is in DESIGN-SYSTEM.md.
| Angular | Status |
|---|---|
| 18 | ✅ verified (AOT build) |
| 19 | ✅ verified (AOT build) |
| 20 | ✅ verified (AOT build) |
| 21 | ✅ verified (AOT build) |
| 22 | ✅ verified (AOT build) |
Published in partial-Ivy mode, so the consuming app's compiler links it (not locked to one
runtime); declaration minVersion is 17. scripts/verify-angular.sh <major> scaffolds a real
app on a version, installs the packed tarball, and runs a production AOT build — CI runs it across
all five majors.
- Recurrence covers a pragmatic RRULE subset (FREQ/INTERVAL/COUNT/UNTIL/BYDAY/BYMONTHDAY); ordinal
BYDAY(2MO),BYSETPOS,BYMONTH, and a recurrence editor UI are not yet implemented. - A full editor window and complete keyboard grid navigation are still on the roadmap.
- Timeline resource grouping is single-level (no hierarchy yet).
- Virtual scrolling uses CSS
content-visibility(off-screen rows skip render); it is not a windowed/recycled list. - Luxon/Moment adapters lack a shared parity test suite; Moment format-token parity is unverified.
- No pre-compiled CSS is shipped yet — import the SCSS (see Styling).
See scheduler-plan.md for the full roadmap and CHANGELOG.md for release notes.
MIT