JavaScript Schedule

@lemonadejs/schedule · ✓ 37 contract checks · framework-agnostic · zero dependencies

<Schedule /> — the week/day time-grid scheduler, ported from the v5 plugin (@lemonadejs/schedule). The v5 block is NOT a month calendar: it is a vertical time grid (1px per minute) with day columns, and that exact model is preserved here:

  • views: type ‘week’ (Sun–Sat) | ‘weekdays’ (Mon–Fri) | ‘day’; weekly mode swaps real dates for abstract weekdays (recurring template schedules — events carry weekday instead of date)
  • grid: minutes per row (default 15, row height = grid px); snap: create/resize step in minutes (defaults to grid)
  • drag-create on empty cells, drag-move (top 25px zone) and drag-resize (bottom 5px zone) of events, with conflict blocking when overlap=false and read-only hour ranges
  • selection (click, Ctrl+click multi), keyboard: arrows walk events, Enter opens the editor for the selected event, N creates an event at the first free slot, Alt+arrows move and Shift+Up/Down resize the selected event (the same commit path as the drag gestures), Delete removes, Ctrl+C/V copy/paste (+1 row shift), Ctrl+Z/Y undo/redo (full history: add/update/delete/setData)
  • validrange hides hours outside the window; readonlyrange disables (striped) hour ranges; now-pointer line on today’s column
  • the v5 Event editor (dist/event.js — a Modal with title/when/ start/end/location/color palette) is built in, composing @lemonadejs/modal; it opens on double click and after drag-create (v5’s onedition moments) — disable with editor=false

v5 → v6 mapping: validRange → validrange, readOnlyRange → readonlyrange, render() → api.refresh(); onchangeevent(record, oldValue, newValue) keeps its v5 name — it fires after every committed event update (drag-move/resize commit, editor save, api.updateEvent), alongside onupdate (same payload); onbeforechangeevent(record, oldValue, newValue) is its cancellable pre-flight — return false and nothing mutates (the drag snaps back). v5 fired onbeforechangeevent at gesture START with the raw drag state; the v6 gesture-start veto is onbeforedrag({ kind, record }) — onbeforechangeevent now guards the concrete change at COMMIT, so it sees the real oldValue/newValue. Callbacks drop the leading self argument (pure components, no this); document.dictionary → weekdays prop; getEvent returns the RECORD (not a DOM node). Data is BY REFERENCE: mutate the array (or a record) and touch() — the grid re-renders once.

Example

live
import { html } from 'lemonadejs';
import Schedule from '@lemonadejs/schedule';

// This week's dates (offset from Sunday, local time), so the example is always populated
const day = (offset) => {
    const d = new Date();
    d.setDate(d.getDate() - d.getDay() + offset);
    return [d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0')].join('-');
};

const App = (props, { state }) => {
    const events = state([
        { date: day(1), start: '09:00', end: '09:30', title: 'Standup', color: '#3f51b5' },
        { date: day(1), start: '11:00', end: '12:30', title: 'Design review', color: '#009688', location: 'Room 2' },
        { date: day(2), start: '14:00', end: '16:00', title: 'Deep work', color: '#795548' },
        { date: day(3), start: '10:00', end: '11:00', title: 'Customer call', color: '#ff9800', location: 'Zoom' },
        { date: day(4), start: '15:00', end: '17:00', title: 'Sprint planning', color: '#e91e63', location: 'Room 4' },
        { date: day(5), start: '13:00', end: '14:00', title: 'All-hands', color: '#9c27b0', readonly: true },
    ]);
    const note = state('Drag empty cells to create an event, drag events to move or resize, double-click to edit.');

    return html`<div>
        <${Schedule} data="${events}" type="weekdays" validrange="${['08:00', '18:00']}" grid="30"
            oncreate="${(added) => (note.value = 'Created: ' + added.map((e) => e.title).join(', '))}"
            onupdate="${(record) => (note.value = `${record.title} is now ${record.date} ${record.start}–${record.end}`)}"
            ondelete="${(record) => (note.value = 'Deleted: ' + record.title)}" />
        <p style="font-size:13px">${note}</p>
    </div>`;
};

Installation

npm install @lemonadejs/schedule
import Schedule from '@lemonadejs/schedule';
import '@lemonadejs/schedule/style.css';

Three deployment forms, one component:

html`<${Schedule} />`                       // by value (no registration)
setComponents({ Schedule });               // then <Schedule /> by name anywhere
createWebComponent(Schedule);              // <lm-schedule> in plain HTML/any framework

Props

Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.

PropTypeDefaultDescription
dataarrayScheduleEvent[] BY REFERENCE (mutate + touch())
valuestring''anchor date ‘YYYY-MM-DD’ (default: today)
typestring"week"’week'
weeklybooleanfalseabstract weekday columns (no dates)
gridnumber15minutes per row (row height = grid px)
snapnumber0create/resize step in minutes (0 = grid)
overlapbooleanfalseallow overlapping events (true staggers them)
validrangearrayvisible hours, e.g. [‘08:00’,‘20:00’] (v5: validRange)
readonlyrangearraydisabled hours: [‘a’,‘b’] or [[‘a’,‘b’],…] (v5: readOnlyRange)
editorbooleantruebuilt-in event editor (v5 shipped it as lm-event)
weekdaysarray7 weekday names (v5: document.dictionary)

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onchange — (data) — any user/api change to the data
  • oncreate — (events) — events added
  • onbeforecreate — (events) — return false to cancel
  • onbeforeinsert — (event) — drag-create template; false cancels, object replaces
  • onupdate — (record, oldValue, newValue) — alias of onchangeevent
  • onchangeevent — (record, oldValue, newValue) — v5 name, after a committed update
  • onbeforechange — ({ action, … }) — return false to cancel
  • onbeforechangeevent — (record, oldValue, newValue) — return false to cancel the update
  • onbeforedrag — ({ kind, record }) — false cancels the gesture at its start
  • ondelete — (record) — per removed event
  • ondblclick — (record)
  • onedition — (record) — editor moment (dblclick / after drag-create)
  • onerror — (message)

API

import { ref } from 'lemonadejs';
const schedule = ref();
html`<${Schedule} ref="${schedule}" />`;
// schedule.current.addEvents(...)  ·  schedule.current.updateEvent(...)  ·  schedule.current.deleteEvents(...)  ·  schedule.current.getData(...)  ·  schedule.current.setData(...)  ·  schedule.current.getEvent(...)  ·  schedule.current.getSelected(...)  ·  schedule.current.resetSelection(...)  ·  schedule.current.setRange(...)  ·  schedule.current.setReadOnly(...)  ·  schedule.current.undo(...)  ·  schedule.current.redo(...)  ·  schedule.current.next(...)  ·  schedule.current.prev(...)  ·  schedule.current.today(...)  ·  schedule.current.openEditor(...)  ·  schedule.current.refresh(...)
  • addEvents()
  • updateEvent()
  • deleteEvents()
  • getData()
  • setData()
  • getEvent()
  • getSelected()
  • resetSelection()
  • setRange()
  • setReadOnly()
  • undo()
  • redo()
  • next()
  • prev()
  • today()
  • openEditor()
  • refresh()

Styling

All classes follow the lm-schedule-* convention; visual variants are data-* attributes on the root. Override freely — there is no styling engine to fight.

Contract

The machine-readable schema ships with the package:

import contract from '@lemonadejs/schedule/contract.json';

verify.json carries the conformance proof produced by verify(Schedule).

Looking for the v5 plugin? See the archived v5 documentation.