Building Event-Driven Applications with a JavaScript Calendar
By SimpleCalendarJS Team
A calendar is more than a display widget. It is a user interface for time — and the real value it delivers comes not from rendering dates, but from what happens when users interact with it. Click an empty slot and you want a creation form. Click an existing event and you want a detail panel. Switch weeks and you want fresh data from your API.
That is event-driven architecture applied to scheduling UIs. Tools like Calendly and Cal.com are, at their core, a set of event handlers wired to a backend. The calendar renders; callbacks drive everything else. With SimpleCalendarJS, you get that same model in a lightweight, embeddable package. This post walks through the four core callbacks, two complete real-world patterns, and the optimistic update approach that keeps your UI feeling fast.
The Four Core Callbacks
Every interactive calendar application is built on the same small set of hooks. Nail these and the rest is business logic.
1. onEventClick — View or Edit an Event
Fired when a user clicks an existing event block. Use it to open a details panel, populate an edit form, or trigger a side drawer.
const calendar = new SimpleCalendarJs('#calendar', { onEventClick: (event) => { openModal({ id: event.id, title: event.title, start: event.start, end: event.end, meta: event.meta, }); }, });
The event object carries everything you stored when you originally loaded the event — including any custom meta fields — so you can drive downstream UI without a second API call.
2. onSlotClick — Create a New Event
Fired when a user clicks an empty calendar cell. This is where scheduling UIs begin. Pre-fill your creation form with the clicked date so the user never has to type it.
const calendar = new SimpleCalendarJs('#calendar', { onSlotClick: (date) => { openCreateForm({ defaultDate: date }); }, });
This mirrors exactly what Calendly's booking page does: the available slot drives the form, not the other way around.
3. onViewChange — Load the Right Data Per View
Daily, weekly, and monthly views have very different data needs. A day view might show granular 15-minute slots; a month view might show only event counts. Use this callback to adjust your fetch strategy.
const calendar = new SimpleCalendarJs('#calendar', { onViewChange: (view) => { // 'day' | 'week' | 'month' if (view === 'month') { setFetchGranularity('summary'); } else { setFetchGranularity('full'); } }, });
4. onDateChange — Sync URL and State with Navigation
When users navigate to a new date range, push that range into the URL so the view is bookmarkable and shareable — the same pattern used by Google Calendar.
const calendar = new SimpleCalendarJs('#calendar', { onDateChange: (start, end) => { const params = new URLSearchParams({ from: start.toISOString(), to: end.toISOString(), }); history.replaceState(null, '', `?${params.toString()}`); }, });
On page load, read those params back to restore the calendar to the correct position.
Real-World Pattern 1: Appointment Booking System
A booking system has three requirements: show availability, capture new bookings on slot click, and support cancellation on event click. Here is a full working skeleton.
const calendar = new SimpleCalendarJs('#calendar', { fetchEvents: async (start, end) => { const res = await fetch( `/api/bookings?from=${start.toISOString()}&to=${end.toISOString()}` ); const data = await res.json(); // Map API shape to the SimpleCalendarJS event shape return data.map((booking) => ({ id: booking.id, title: booking.customerName, start: new Date(booking.startsAt), end: new Date(booking.endsAt), color: booking.status === 'confirmed' ? '#22c55e' : '#f59e0b', meta: { status: booking.status, phone: booking.phone }, })); }, onSlotClick: (date) => { // Show a "New Booking" form pre-filled with the selected time openBookingForm({ defaultDate: date, onConfirm: async (formData) => { await createBooking({ ...formData, startsAt: date }); calendar.refresh(); // Re-fetch to show the new event }, }); }, onEventClick: (event) => { openBookingDetail({ booking: event, onCancel: async () => { await fetch(`/api/bookings/${event.id}`, { method: 'DELETE' }); calendar.refresh(); }, }); }, });
The key architectural decision here: fetchEvents is your single source of truth. You never manually splice events into the calendar — you call calendar.refresh() and let fetchEvents rebuild the state from the server. This keeps the calendar and backend in sync without managing a local event array.
Real-World Pattern 2: Team Task Planner
A team planner adds one layer on top: events belong to people, and you want to filter by assignee and colour-code by team member at a glance.
const TEAM_COLORS = { alice: '#6366f1', bob: '#ec4899', carol: '#14b8a6', }; let activeFilter = null; // null = show everyone const calendar = new SimpleCalendarJs('#calendar', { fetchEvents: async (start, end) => { const url = new URL('/api/tasks', location.origin); url.searchParams.set('from', start.toISOString()); url.searchParams.set('to', end.toISOString()); if (activeFilter) url.searchParams.set('assignee', activeFilter); const tasks = await fetch(url).then((r) => r.json()); return tasks.map((task) => ({ id: task.id, title: `[${task.assignee}] ${task.title}`, start: new Date(task.dueStart), end: new Date(task.dueEnd), color: TEAM_COLORS[task.assignee] ?? '#94a3b8', meta: { assignee: task.assignee, priority: task.priority }, })); }, onEventClick: (event) => { openTaskDetail(event); }, }); // Wire up your filter UI to refresh the calendar document.querySelectorAll('[data-member]').forEach((btn) => { btn.addEventListener('click', () => { activeFilter = btn.dataset.member === activeFilter ? null : btn.dataset.member; calendar.refresh(); }); });
Because activeFilter is captured in the closure that fetchEvents reads, toggling a filter button and calling calendar.refresh() is all it takes to re-fetch with the new constraint. No separate event list to manage.
Syncing with Your Backend: The Optimistic Update Pattern
Calling calendar.refresh() after every mutation works, but it causes a visible flicker: the event disappears briefly while the fetch completes. For status changes or quick edits, that lag hurts perceived performance.
The fix is the optimistic update pattern: update the calendar immediately, persist to the API in the background, and roll back only if the API call fails. This is the same technique React Query's onMutate / onError hooks implement — and what makes tools like Linear and Notion feel instant even under unreliable network conditions.
const calendar = new SimpleCalendarJs('#calendar', { onEventClick: (event) => { openStatusToggle({ event, onToggle: async (newStatus) => { // 1. Update local state immediately const previous = { ...event }; calendar.updateEvent(event.id, { color: newStatus === 'confirmed' ? '#22c55e' : '#f59e0b', meta: { ...event.meta, status: newStatus }, }); try { // 2. Persist to the server await fetch(`/api/bookings/${event.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }), }); } catch (err) { // 3. Roll back on failure calendar.updateEvent(event.id, previous); showToast('Failed to update — changes reverted.'); } }, }); }, });
Apply this pattern to any mutation where the happy path is overwhelmingly likely. Reserve full re-fetches for operations that change the total set of visible events (creates and deletes), and use local updates for field changes on existing events.
Callback Reference
| Callback | Triggered when | Receives |
|---|---|---|
fetchEvents | View loads or date range changes | start: Date, end: Date |
onEventClick | User clicks an existing event | event object |
onSlotClick | User clicks an empty calendar cell | date: Date |
onViewChange | User switches between day/week/month | view: string |
onDateChange | User navigates to a new date range | start: Date, end: Date |
Summary
- A calendar's callbacks are its API surface. Design your application logic around them, not around DOM manipulation.
fetchEventsshould be your single source of truth. Re-fetch rather than manually patching a local event array.onSlotClickandonEventClickare the two primary entry points for user intent — wire your creation and editing flows directly to these.- Use
onDateChangeto keep the URL in sync with the calendar state, making views bookmarkable and shareable. - Adopt optimistic updates for field-level mutations where the error rate is low — update the UI first, persist async, roll back on failure.
Frequently Asked Questions
How do I open a modal when a user clicks a calendar event?
Use the onEventClick callback. It receives the full event object, so you can pass event.id, event.title, and any custom meta fields directly to your modal state — no second API call needed.
How do I pre-fill a booking form with the time slot a user clicked?
Use the onSlotClick callback. It receives a Date object for the clicked cell. Pass that date as the defaultDate to your creation form so users never have to type the time manually.
How do I fetch new events when the user navigates to a different week or month?
Define a fetchEvents(start, end) function in your calendar config. It is called automatically whenever the visible date range changes, so you always fetch only the events relevant to the current view.
How do I implement optimistic updates so the calendar feels instant after a status change?
Call calendar.updateEvent(id, changes) immediately after the user acts, then persist to the API in the background. If the API call fails, call calendar.updateEvent again with the original values to roll back.
Can I implement drag-and-drop rescheduling with a JavaScript calendar?
Yes. Wire an onEventMove or equivalent drag callback to send a PATCH request with the new start and end times. Apply the change optimistically on drag-end, then confirm or roll back based on the API response.
How do I keep the calendar URL in sync so users can share or bookmark a specific week?
Use the onDateChange callback, which fires with the new start and end dates whenever the user navigates. Push those values into the URL with history.replaceState, then read them back on page load to restore the correct view.
Add a calendar to your app today
Free for personal projects. $49/year or $199 lifetime per commercial project.
