@datelane/core
June 21, 2026 · View on GitHub
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).
Highlights
- 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.
Install
npm i @datelane/core
# optional — only if you choose that date adapter:
npm i luxon # or: npm i moment
Peer deps: @angular/core and @angular/common >=18.0.0 <23.0.0. luxon/moment are
optional peers.
Quick start (standalone, Native dates)
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 */ }
}
Styling
The package ships SCSS (tokens + theme + component styles); there is no pre-compiled CSS yet.
Import it once. Either add it to angular.json:
// angular.json → projects.<app>.architect.build.options.styles
"styles": [
"src/styles.scss",
"node_modules/@datelane/core/styles/scheduler.scss"
]
…or @use it from your global stylesheet:
/* src/styles.scss */
@use '@datelane/core/styles/scheduler';
Views
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.
Data & FieldMap
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.
Inputs
| 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 |
Outputs
| 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].
Editing model (bring your own form)
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 & grouping (Timeline)
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.
Auto-scroll
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.
Date adapters
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' }))];
Localizing UI strings
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).
Header navigation
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.
All-day band (Day / Week / Work Week)
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).
Non-standalone apps
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 {}
Theming
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 support
| 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.
Limitations
- 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.
License
MIT