calendar-tsx

Quick start

A month calendar rendered to the DOM, without a framework.

main.ts
import type { Calendar_DayGridLayout } from '@calendar-tsx/core';
import { createCalendar, calendarViews, calendarFeatures, dayGridView, timeGridView } from '@calendar-tsx/core';
import '@calendar-tsx/themes/default.css';
import { Temporal } from 'temporal-polyfill';

type Booking = { title: string };

const features = calendarFeatures({});
const views = calendarViews({
  features: features,
  views: {
    month: dayGridView({ period: 'month', maxEventsPerDay: 3 }),
    week: timeGridView({ days: 7 }),
  },
});

const calendar = createCalendar({
  timeZone: 'America/New_York',
  views,
  features,
  view: 'month',
  date: Temporal.Now.plainDateISO(),
  events: [{ id: 'standup', kind: 'allDay', start: Temporal.Now.plainDateISO(), data: { title: 'Standup' } }],
  // `view` and `date` are controlled: apply the next ones yourself
  onNavigate: next => calendar['~update']({ ...calendar.options, view: next.view, date: next.date }),
});

// the attributes and custom properties the themes read
const part = (tag: string, name: string, attributes: Record<string, string> = {}) => {
  const element = document.createElement(tag);

  element.dataset['scope'] = 'calendar';
  element.dataset['part'] = name;
  for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, value);

  return element;
};

const renderDayGrid = (layout: Calendar_DayGridLayout<Booking>) => {
  const root = part('div', 'day-grid-root', { role: 'region', 'aria-label': 'Calendar' });

  for (const section of layout.sections) {
    const grid = part('div', 'day-grid-section', { role: 'grid' });
    grid.style.setProperty('--cal-columns', String(Math.max(1, section.columnCount)));
    for (const week of section.rows) {
      const row = part('div', 'day-grid-row', { role: 'row' });

      row.style.setProperty('--cal-rows', String(week.stackCount));
      for (const day of week.days) {
        const cell = part('div', 'day-grid-day', { role: 'gridcell' });

        if (day.isToday) cell.dataset['today'] = '';
        if (day.isOutside) cell.dataset['outside'] = '';
        cell.textContent = String(day.date.day);
        row.append(cell);
      }
      for (const segment of week.segments) {
        const bar = part('button', 'day-grid-row-event', { type: 'button' });

        bar.style.setProperty('--cal-col-start', String(segment.colStart));
        bar.style.setProperty('--cal-col-span', String(segment.colSpan));
        bar.style.setProperty('--cal-row', String(segment.row));
        bar.textContent = segment.event.data.title;
        row.append(bar);
      }
      grid.append(row);
    }
    root.append(grid);
  }

  return root;
};

const container = document.querySelector('#calendar')!;

const render = () => {
  const { layout } = calendar.store.state;

  if (layout.kind === 'dayGrid') container.replaceChildren(renderDayGrid(layout));
};

calendar['~mount'](); // starts the clock
calendar.store.subscribe(render);
render();

document.querySelector('#next')!.addEventListener('click', () => calendar.api.next());

What it does

  1. createCalendar derives the state from the options. view and date are controlled, so onNavigate passes the next ones back in through ~update.
  2. ~mount starts the clock; the returned function stops it.
  3. store.subscribe re-renders when the state changes. store.state.layout is the model for the active view.
  4. The renderer writes data-scope, data-part, state attributes and --cal-* custom properties, the contract the themes read, so the default theme lays it out.

Let the types infer

Don't pass type arguments such as createCalendar<Booking>(…): TypeScript then stops inferring the rest, and view keys widen to string. Type events instead, for example as Calendar_EventInput<Booking>[].