CalendarComponent

Hex.pm Docs

Phoenix components for EventCalendar (v5.17.2): a full-sized, drag & drop event calendar with day, week, month, list, resource and timeline views.

Installation

Add calendar_component to your dependencies in mix.exs:

def deps do
  [
    {:calendar_component, "~> 0.3.0"}
  ]
end

JavaScript

Register the hook in assets/js/app.js:

import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import CalendarHooks from "calendar_component"

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {...CalendarHooks}  // merge with your other hooks: {...CalendarHooks, ...MyHooks}
})
liveSocket.connect()

Phoenix resolves calendar_component from deps/ through the NODE_PATH set in the default esbuild configuration.

CSS

Import the EventCalendar stylesheet in assets/css/app.css:

@import "../../deps/calendar_component/priv/static/assets/main.css";

Add the ec-dark class to any parent element (or ec-auto-dark to follow the system preference) to enable the built-in dark theme, or override the CSS variables:

.ec {
  --ec-bg-color: #22272e;
  --ec-text-color: #adbac7;
}

Usage with LiveView

defmodule MyAppWeb.CalendarLive do
  use MyAppWeb, :live_view

  import LiveCalendar.Components

  def mount(_params, _session, socket) do
    events = [
      %{id: 1, title: "Planning", start: ~N[2026-10-05 09:00:00], end: ~N[2026-10-05 10:00:00]},
      %{id: 2, title: "Offsite", start: ~D[2026-10-14], allDay: true}
    ]

    {:ok, assign(socket, events: events)}
  end

  def render(assigns) do
    ~H"""
    <.calendar
      id="calendar"
      events={@events}
      options={%{
        view: "dayGridMonth",
        headerToolbar: %{start: "prev,next today", center: "title", end: "dayGridMonth,timeGridWeek,listWeek"},
        firstDay: 1,
        nowIndicator: true
      }}
    />
    """
  end

  def handle_event("event_clicked", %{"id" => id}, socket), do: {:noreply, socket}
  def handle_event("date_clicked", %{"dateStr" => date}, socket), do: {:noreply, socket}
  def handle_event("month_changed", %{"month" => month, "year" => year}, socket), do: {:noreply, socket}
end

The container is rendered with phx-update="ignore": LiveView never touches the calendar DOM, while changes to events and options are still applied by the hook. Only options whose value changed are re-applied, so unrelated re-renders don't reset the date or the view the user navigated to.

Callbacks

Attribute EventCalendar callback Default server event
on_event_click eventClick "event_clicked"
on_date_click dateClick "date_clicked"
on_month_change datesSet "month_changed"
on_event_drop eventDrop none
on_event_resize eventResize none
on_select select none

Each attribute accepts:

<.calendar
  id="calendar"
  events={@events}
  on_event_click={JS.push("select_event") |> JS.show(to: "#event-modal")}
  on_date_click={false}
  on_month_change="range_changed"
/>

Payloads

Dates are sent in two formats:

Callback Payload keys
on_event_click id, event, view
on_date_click date, dateStr, allDay, resource, view
on_month_change month, year, start, end, startStr, endStr, view
on_event_drop id, event, oldEvent, delta, oldResource, newResource, view
on_event_resize id, event, oldEvent, startDelta, endDelta, view
on_select start, end, startStr, endStr, allDay, resource, view

month and year come from the current period (view.currentStart), so in month views they match the displayed month even though the grid starts in the previous one.

Drag & drop, resize and selection

def render(assigns) do
  ~H"""
  <.calendar
    id="calendar"
    events={@events}
    options={%{view: "timeGridWeek", editable: true, selectable: true}}
    on_event_drop="event_changed"
    on_event_resize="event_changed"
    on_select="range_selected"
  />
  """
end

def handle_event("event_changed", %{"id" => id, "event" => event}, socket) do
  case Events.reschedule(id, event["start"], event["end"]) do
    {:ok, _event} -> {:noreply, assign(socket, events: Events.list())}
    # Moves the event back to where it was
    {:error, _reason} -> {:reply, %{revert: true}, socket}
  end
end

def handle_event("range_selected", %{"start" => start, "end" => finish}, socket) do
  {:noreply, assign(socket, new_event: %{start: start, end: finish})}
end

Driving the calendar from the server

socket
|> CalendarComponent.push_command("calendar", :goto_date, [~D[2026-12-01]])
|> CalendarComponent.push_command("calendar", :set_option, ["view", "timeGridDay"])

Commands: :goto_date, :next, :prev, :today, :refetch_events, :refetch_resources, :unselect and :set_option. Changing an option in options works as well, it is applied whenever its value changes.

Custom buttons

Custom toolbar buttons can push an event when clicked:

options = %{
  headerToolbar: %{start: "prev,next today", center: "title", end: "addEvent"},
  customButtons: %{addEvent: %{text: "New event", push: "new_event_clicked"}}
}

The handler receives %{"button" => "addEvent", "view" => view}.

JavaScript callbacks

Callback options can also reference global JavaScript functions, which run before the event is pushed to the server:

options={%{eventDidMount: "MyApp.Calendar.addTooltip", eventClick: "MyApp.Calendar.track"}}

Usage with controllers

Use static_calendar/1 in regular controller templates:

<.static_calendar
  id="calendar"
  events={@events}
  options={%{
    view: "dayGridMonth",
    eventClick: "MyApp.Calendar.onEventClick",
    dateClick: "MyApp.Calendar.onDateClick"
  }}
/>
// assets/js/app.js
import {initStaticCalendars} from "calendar_component"   // or "calendar_component/static"

window.MyApp = {
  Calendar: {
    onEventClick(info) { window.location = `/events/${info.event.id}` },
    onDateClick(info) { window.location = `/events/new?date=${info.dateStr}` }
  }
}

initStaticCalendars()  // optional, calendars are also initialized on page load

Callback options given as strings are resolved to global functions. Short function bodies receiving info are also accepted (max. 200 characters, keywords such as function, eval or setTimeout are rejected), but global function references are recommended. See the static calendar guide and security notes.

initStaticCalendar(el), getStaticCalendar(el), updateStaticCalendarEvents(el, events) and destroyStaticCalendar(el) manage single calendars.

Events

Events are maps that are encoded with Jason (structs must be converted to maps first):

%{
  id: 1,
  title: "Meeting",
  start: ~N[2026-10-05 09:00:00],     # Date, NaiveDateTime, DateTime or ISO8601 string
  end: ~N[2026-10-05 10:00:00],       # optional
  allDay: false,                      # optional
  color: "#3b82f6",                   # optional, also backgroundColor / textColor
  resourceId: 1,                      # or resourceIds: [1, 2]
  display: "auto",                    # "background" for background events
  extendedProps: %{room: "A"}         # any extra data, sent back in payloads
}

Options

Pass any EventCalendar option in options. Some common ones:

%{
  view: "timeGridWeek",
  date: "2026-10-01",
  locale: "es",
  firstDay: 1,
  height: "700px",
  slotMinTime: "07:00",
  slotMaxTime: "20:00",
  hiddenDays: [0, 6],
  dayMaxEvents: true,
  nowIndicator: true,
  timeZone: "UTC",
  headerToolbar: %{start: "prev,next today", center: "title", end: "dayGridMonth,timeGridWeek"},
  views: %{timeGridFourDay: %{type: "timeGridDay", duration: %{days: 4}}}
}

Available views: dayGridDay, dayGridWeek, dayGridMonth, timeGridDay, timeGridWeek, listDay, listWeek, listMonth, listYear, resourceTimeGridDay, resourceTimeGridWeek, resourceTimelineDay, resourceTimelineWeek, resourceTimelineMonth, resourceTimelineYear, plus custom views defined in views with a type. All plugins are loaded, so any view can be used without extra configuration.

For compatibility, initialView is accepted as an alias of view and headerToolbar accepts left/right as aliases of start/end.

Resources

options = %{
  view: "resourceTimelineWeek",
  resources: [
    %{id: 1, title: "Room A"},
    %{id: 2, title: "Room B", children: [%{id: 3, title: "Booth B1"}]}
  ]
}

events = [%{id: 1, title: "Workshop", start: "2026-10-05T09:00:00", end: "2026-10-05T12:00:00", resourceIds: [1, 3]}]

resources and eventSources can also be URLs (%{url: "/api/resources"}), refreshed with push_command(socket, id, :refetch_resources) / :refetch_events.

Upgrading from 0.2.x

Development

mix setup                 # deps, esbuild, assets and format
cd assets && npm install  # EventCalendar sources
mix assets.build          # builds priv/static/assets
mix test                  # Elixir tests
cd assets && npm test     # JavaScript tests

Acknowledgments

This library builds on EventCalendar by Vlad Kurko. Thanks to the EventCalendar project for providing a lightweight, flexible calendar core.

License

MIT. See the LICENSE file for details.