Hapio Calendar

Migrating to Hapio Calendar

Move a calendar app to Hapio Calendar by changing an import, keep its options, and then adopt Hapio's own API at your pace.

Hapio Calendar is an independent calendar, not a fork of another. To make moving easy it has a migration component, EventCalendar, that takes the React options, callbacks and payloads of the calendar library you migrate from by their names and renders Hapio's HapioCalendar with them. There is one render path: the migration component is the same calendar, with the same keyboard support, accessibility and time-zone handling, configured from your existing options.

1. Add the migration component

npx shadcn@latest add @hapio/event-calendar

This adds components/event-calendar.tsx and the hapio-calendar block it renders, and installs @hapio/calendar-compat. Load the Temporal polyfill as Installation shows.

2. Change the import

Replace your current calendar component with EventCalendar and keep your options. The plugins go: every view is built in, so plugins is ignored. See it running in the migration example.

components/bookings-calendar.tsx
'use client'

import type { DatesSetInfo, EventClickInfo, EventInput } from '@hapio/calendar-compat'

import { EventCalendar } from '@/components/event-calendar'

interface BookingProps {
  readonly status: 'confirmed' | 'canceled'
}

export function BookingsCalendar({
  events,
  onRange,
  onOpen,
}: {
  events: readonly EventInput<BookingProps>[]
  onRange: (startStr: string, endStr: string) => void
  onOpen: (id: string) => void
}) {
  const eventClick = (info: EventClickInfo<BookingProps>) => {
    info.jsEvent.preventDefault() // stay on the page instead of following `url`
    onOpen(info.event.id)
  }
  const datesSet = (info: DatesSetInfo) => onRange(info.startStr, info.endStr)
  return (
    <EventCalendar<BookingProps>
      initialView="timeGridWeek"
      availableViews={['timeGridWeek', 'timeGridDay', 'dayGridMonth', 'listWeek']}
      timeZone="Europe/Stockholm"
      firstDay={1}
      slotMinTime="07:00:00"
      nowIndicator
      navLinks
      events={events}
      eventClick={eventClick}
      datesSet={datesSet}
      eventClass={(info) => (info.event.extendedProps.status === 'canceled' ? 'line-through' : '')}
      height="max(36rem, calc(100svh - 17rem))"
    />
  )
}
  • Types: @hapio/calendar-compat exports your current calendar's option and payload types under their names (EventInput, EventClickInfo, DatesSetInfo, EventApi, …), generic in your extendedProps type, and EventCalendarProps declares every option the component maps, so those keep compiling (satisfies EventCalendarProps). An option marked reported on Options is not declared: in a typed app it is a type error ("Object literal may only specify known properties"), which lists what step 4 removes (Differences).
  • Development warnings: a reported option that reaches the component (from JavaScript, or through an object not checked against the type) is ignored and listed once in the browser console, with a link to this guide.
  • Uncontrolled, as before: initialView and initialDate seed the calendar once; re-create it with a key to reset it. A dropped or resized event stays where the user left it until your events change it (Differences).
  • The size: height takes any CSS length or pixels and sizes the whole calendar, toolbar included.

3. Interactions are wrappers

Your current calendar's interaction plugin made calendars selectable, editable and droppable. In Hapio each is an opt-in item whose wrapper gives the calendars inside it the code, so a read-only calendar stays small:

Your optionsWrap the calendar in
selectable, select, dateClick in week and day views<CalendarSelection> (calendar-selection)
editable, eventDrop, eventResize, eventChange<CalendarEditing> (calendar-editing, selection included)
droppable, drop, eventReceive<CalendarExternalDrops> (calendar-external-draggable) inside either

Every editable event also gets its actions menu (move, change the time and resize without dragging), which your current calendar does not have. See Event actions.

4. Replace what has no counterpart

The options marked reported on Options have no counterpart, so they go in this step: a typed app's compiler lists them as type errors, and in JavaScript the development warnings do. Remove each one, or replace it as its row says:

  • The toolbar DSL (headerToolbar, footerToolbar, buttons): keep the built-in toolbar, or build your own with useCalendarController() from @hapio/calendar-compat/react, which replaces it:
components/calendar-with-header.tsx
'use client'

import type { EventInput } from '@hapio/calendar-compat'
import { useCalendarController } from '@hapio/calendar-compat/react'

import { EventCalendar } from '@/components/event-calendar'
import { Button } from '@/components/ui/button'

export function CalendarWithHeader({ events }: { events: readonly EventInput[] }) {
  // Replaces `headerToolbar`: your own header, driving the calendar.
  const controller = useCalendarController()
  const buttons = controller.getButtonState()
  return (
    <>
      <header className="flex items-center gap-2">
        <Button
          type="button"
          variant="outline"
          onClick={controller.prev}
          aria-disabled={buttons.prev.isDisabled}
        >
          {buttons.prev.hint}
        </Button>
        <h2>{controller.view?.title}</h2>
        <Button
          type="button"
          variant="outline"
          onClick={controller.next}
          aria-disabled={buttons.next.isDisabled}
        >
          {buttons.next.hint}
        </Button>
        <Button type="button" onClick={controller.today} aria-disabled={buttons.today.isDisabled}>
          {buttons.today.text}
        </Button>
      </header>
      <EventCalendar controller={controller} initialView="dayGridMonth" events={events} />
    </>
  )
}
  • The imperative API (getApi() mutations, addEvent, setProp, refetchEvents): Hapio is declarative. Change your events, and the calendar follows.
  • Draggable for external elements: use CalendarDraggable or useExternalDraggable (Dragging in); its data is what eventReceive reads as the event.
  • Recurring events need the recurrence engine: see Recurrence.

Options maps every option and callback, and Differences lists what behaves differently and what is not supported.

5. Then, at your pace

EventCalendar is a thin file over HapioCalendar. When you want Hapio's own API (controlled view and date, accessor mode, typed data, rules with reasons, recurrence scopes), replace it with HapioCalendar one calendar at a time; both live side by side. Start with Your first calendar.