@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.0 pre-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.

FactoryViewEngine
dayView()Dayvertical-time
weekView()Weekvertical-time
workWeekView()Work Weekvertical-time
monthView()Monthcalendar-grid
yearView()Yearyear-grid
agendaView()Agendalist
monthAgendaView()Month Agendamini-calendar + list
timelineDayView()Timeline Dayhorizontal-time
timelineWeekView()Timeline Weekhorizontal-time
timelineWorkWeekView()Timeline Work Weekhorizontal-time
timelineMonthView()Timeline Monthhorizontal-time
timelineYearView()Timeline Yearhorizontal-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

InputTypeDefaultNotes
activeViewSchedulerViewType'week'two-way (activeViewChange)
viewDateadapter datetodaytwo-way (viewDateChange)
eventsRecord<string, unknown>[][]raw records
fieldMapFieldMaprequired to render events
viewsViewDescriptor[][]from the view factories
resourcesResourceDefinition[][]Timeline rows
groupingGroupingConfig{ resources: ['name'] }
readonlybooleanfalsedisables drag/resize + quick-view actions
rowAutoHeightbooleanfalseTimeline: grow rows vs +N more
agendaDaysCountnumber7Agenda span
hideEmptyAgendaDaysbooleanfalseAgenda
showQuickViewbooleantruebuilt-in popover on activation
autoScrollbooleantruescroll to first event (see below)
scrollHournumbertime grids: scroll to this hour instead
heightstring'600px'required px height for Agenda / Month Agenda
widthstring'100%'
dir'ltr' | 'rtl''ltr'RTL mirrors the layout

Outputs

OutputPayloadWhen
eventClickSchedulerChangeany event activation (always)
eventEditSchedulerChangequick-view Edit → open your form
eventDeleteSchedulerChangequick-view Delete
eventChangeSchedulerChangeafter a drag-move or resize
eventCreateSchedulerChangereserved for the editor (pending)
cellClick{ date, resourceId? }empty slot/cell — open your create form
navigateNavigateEventdate navigation / drill-through
viewChangeSchedulerViewTypeactive 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:

  1. always emits (eventClick), and
  2. (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

AngularStatus
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