# CalendarComponent
[
liveSocket.connect()
```s.io/hexpm/v/calendar_component.svg)](https://hex.pm/packages/calendar_component)
[](https://hexdocs.pm/calendar_component)
Phoenix LiveView component library that renders an interactive calendar powered by EventCalendar and a LiveView JavaScript hook. It ships as a library (not a full Phoenix app) and compiles colocated JS/CSS assets.
## Usage
### With LiveView
Render the calendar component in HEEx and wire events with Phoenix LiveView JS:
```elixir
alias Phoenix.LiveView.JS
~H"""
<.calendar
id="calendar"
events={@events}
on_event_click={JS.push("event_clicked", value: %{id: event.id})}
on_date_click={JS.push("date_clicked", value: %{date: date})}
on_month_change={JS.push("month_changed", value: %{month: month})}
/>
"""
```
Server-side handlers (example):
```elixir
@impl true
def handle_event("event_clicked", %{"id" => id}, socket), do: {:noreply, socket}
@impl true
def handle_event("date_clicked", %{"date" => date}, socket), do: {:noreply, socket}
@impl true
def handle_event("month_changed", %{"month" => month, "year" => year}, socket), do: {:noreply, socket}
```
### With Regular Phoenix Controllers
For traditional Phoenix controllers (without LiveView), use the static calendar component:
```elixir
# In your controller
defmodule MyAppWeb.EventController do
use MyAppWeb, :controller
import LiveCalendar.Components
def index(conn, _params) do
events = [
%{id: 1, title: "Meeting", start: "2025-08-01T09:00:00"},
%{id: 2, title: "Demo", start: "2025-08-02"}
]
render(conn, "index.html", events: events)
end
end
```
In your template:
```elixir
# events/index.html.heex
<.static_calendar
id="static-calendar"
events={@events}
options={%{
view: "dayGridMonth",
eventClick: "function(info) {
alert('Event: ' + info.event.title);
}",
dateClick: "function(info) {
console.log('Date clicked:', info.dateStr);
}"
}}
/>
```
The static calendar automatically initializes when the page loads. Events are handled via JavaScript callbacks defined in the options.
Register the JS hook in your app’s LiveSocket (ensure the built asset is loaded so `window.LiveCalendarHooks` exists):
```js
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
const Hooks = window.LiveCalendarHooks || {}
const liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks })
liveSocket.connect()
```
Note: This library compiles a hook at `priv/static/assets/calendar-hooks.js` and CSS at `priv/static/assets/calendar-hooks.css`. Ensure the asset is served/loaded in your host app (or copy the hook code into your app assets and register it).
### Options mapping
- Pass any EventCalendar options via the `:options` assign of `<.calendar ... />`. They are forwarded to the JS calendar.
- Special integration key: `options.lv` lets you rename the server events the hook will push:
- `lv.onEventClick` (default: `"event_clicked"`)
- `lv.onDateClick` (default: `"date_clicked"`)
- `lv.onMonthChange` (default: `"month_changed"`)
- If you provide JS handlers in `options.eventClick`, `options.dateClick`, or `options.datesSet`, the hook composes them and still pushes to LiveView.
- The hook auto-picks plugins based on `options.view` (`timeGrid*`, `dayGrid*`, `list*`). You can also pass `options.plugins` explicitly if needed.
Implemented features in hook/component:
- Container render with `phx-hook="LiveCalendar"` and data-json for `events`/`options`.
- JS initialization with `createCalendar(...)` and plugin selection.
- LiveView push for `eventClick`, `dateClick`, `datesSet` (month change).
- Runtime updates via `setOption("events", ...)` and applying changed options on `updated`.
- Cleanup on `destroyed` with `destroyCalendar(...)`.
Refer to `docs/event_calendar.md` for the full list of EventCalendar options and methods. Most options can be passed through `:options`; unsupported options should be raised as issues/PRs if necessary.
## Installation
Add `calendar_component` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:calendar_component, "~> 0.1.5"}
]
end
```
### JavaScript Setup
The library provides JavaScript hooks that need to be registered with Phoenix LiveView, and also supports static calendar initialization for regular Phoenix controllers.
#### For LiveView (Option 1): Direct Import (Recommended)
In your `assets/js/app.js`:
```javascript
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import CalendarHooks from "calendar_component"
// Register the calendar hooks
let liveSocket = new LiveSocket("/live", Socket, {
hooks: CalendarHooks
})
liveSocket.connect()
```
#### For Regular Controllers: Static Calendar
In your `assets/js/app.js`:
```javascript
import { initStaticCalendars } from "calendar_component"
// Initialize static calendars when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
initStaticCalendars()
})
```
#### Option 2: Using the compiled assets
You can also include the compiled JavaScript file directly. First, add the CSS to your `assets/css/app.css`:
```css
@import "../deps/calendar_component/priv/static/assets/calendar-hooks.css";
```
For LiveView, include the LiveView hooks:
```heex
<script src={~p"/assets/deps/calendar_component/priv/static/assets/calendar-hooks.js"} defer></script>
```
For regular controllers, include the static calendar:
```heex
<script src={~p"/assets/deps/calendar_component/priv/static/assets/static-calendar.js"} defer></script>
```
And register accordingly in your JavaScript files.
## Phoenix examples
### LiveView Examples
#### 1) Basic: static render with events
LiveView module:
```elixir
defmodule MyAppWeb.CalendarLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
events = [
%{id: 1, title: "Meeting", start: "2025-08-01T09:00:00"},
%{id: 2, title: "Demo", start: "2025-08-02"}
]
{:ok, assign(socket, events: events)}
end
end
```
HEEx:
```elixir
<.calendar id="calendar" events={@events} />
```
### 2) Handling calendar events (event/date/month)
Use the default event names the hook pushes: "event_clicked", "date_clicked", "month_changed".
```elixir
@impl true
def handle_event("event_clicked", %{"id" => id}, socket) do
{:noreply, put_flash(socket, :info, "Event #{id} clicked")}
end
@impl true
def handle_event("date_clicked", %{"date" => iso}, socket) do
{:noreply, put_flash(socket, :info, "Date #{iso}")}
end
@impl true
def handle_event("month_changed", %{"month" => m, "year" => y}, socket) do
{:noreply, socket}
end
```
### 3) Customize view and toolbar
```elixir
opts = %{
view: "dayGridMonth",
headerToolbar: %{start: "title", end: "today prev,next"},
nowIndicator: true,
firstDay: 1
}
~H"""
<.calendar id="cal_toolbar" events={@events} options={opts} />
"""
```
### 4) Live update events (add after date click)
```elixir
@impl true
def handle_event("date_clicked", %{"date" => iso}, socket) do
new = %{id: System.unique_integer([:positive]), title: "New", start: iso}
{:noreply, update(socket, :events, fn ev -> [new | ev] end)}
end
```
### 5) Rename events the hook pushes (options.lv)
```elixir
opts = %{lv: %{onEventClick: "my_event_click", onDateClick: "my_date_click", onMonthChange: "my_month"}}
~H"""
<.calendar id="cal_lv" events={@events} options={opts} />
"""
@impl true
def handle_event("my_event_click", payload, socket), do: {:noreply, socket}
```
### 6) List view and localization
```elixir
opts = %{
view: "listWeek",
locale: "es",
height: "auto",
dayMaxEvents: true
}
~H"""
<.calendar id="cal_list" events={@events} options={opts} />
"""
```
### 7) Resource Timeline (plugins and resources)
For resource/timeline views, pass plugins explicitly in `options.plugins` and data in `options.resources`.
```elixir
resources = [
%{id: "r1", title: "Room A"},
%{id: "r2", title: "Room B"}
]
events = [
%{id: "e1", title: "Booking", start: "2025-08-03T10:00:00", end: "2025-08-03T12:00:00", resourceId: "r1"}
]
opts = %{
view: "resourceTimelineWeek",
resources: resources,
plugins: :keep_plugins, # see note below
headerToolbar: %{start: "title", end: "today prev,next"}
}
~H"""
<.calendar id="cal_resources" events={events} options={opts} />
"""
# Note: This package auto-selects plugins by `view` (timeGrid/dayGrid/list).
# For ResourceTimeline/ResourceTimeGrid, ensure your asset includes the needed plugins.
# If you use the hooks compiled by this library, consider copying the hook into your app
# and importing the plugins from `@event-calendar/core` you need, for example:
# import { ResourceTimeline, ResourceTimeGrid } from "@event-calendar/core"
# and pass `options.plugins: [ResourceTimeline]`.
```
### 8) Drag & drop and resize editing
Enable `editable: true` and handle `eventDrop`/`eventResize` on the server or client side.
```elixir
opts = %{
view: "timeGridWeek",
editable: true,
eventDurationEditable: true
}
~H"""
<.calendar id="cal_edit" events={@events} options={opts} />
"""
# On the server side, the hook does not push these events automatically.
# You can handle it client-side with `options.eventDrop`/`eventResize` and
# use `pushEvent` manually (by copying/extending the hook in your app) or send via AJAX.
```
### Controller Examples (Static Calendar)
#### 1) Basic calendar in a controller
```elixir
# Controller
defmodule MyAppWeb.CalendarController do
use MyAppWeb, :controller
import LiveCalendar.Components
def show(conn, _params) do
events = [
%{id: 1, title: "Team Meeting", start: "2025-08-15T10:00:00"},
%{id: 2, title: "Project Review", start: "2025-08-16T14:00:00"}
]
render(conn, "show.html", events: events)
end
end
# Template: calendar/show.html.heex
<div class="calendar-container">
<h1>My Events</h1>
<.static_calendar
id="events-calendar"
events={@events}
options={%{view: "dayGridMonth"}}
/>
</div>
```
#### 2) Interactive calendar with JavaScript callbacks
```elixir
# Template with interactive callbacks
<.static_calendar
id="interactive-calendar"
events={@events}
options={%{
view: "dayGridMonth",
eventClick: "function(info) {
window.location.href = '/events/' + info.event.id;
}",
dateClick: "function(info) {
window.location.href = '/events/new?date=' + info.dateStr;
}"
}}
/>
```
#### 3) Advanced calendar with form integration
```elixir
# Controller with form handling
defmodule MyAppWeb.EventController do
use MyAppWeb, :controller
import LiveCalendar.Components
def index(conn, _params) do
events = get_events() # Your function to fetch events
render(conn, "index.html", events: events, changeset: Event.changeset(%Event{}))
end
def create(conn, %{"event" => event_params}) do
case Events.create_event(event_params) do
{:ok, _event} ->
conn
|> put_flash(:info, "Event created successfully")
|> redirect(to: Routes.event_path(conn, :index))
{:error, changeset} ->
events = get_events()
render(conn, "index.html", events: events, changeset: changeset)
end
end
end
# Template: events/index.html.heex
<div class="row">
<div class="col-md-8">
<.static_calendar
id="events-calendar"
events={@events}
options={%{
view: "dayGridMonth",
eventClick: "function(info) {
document.getElementById('event-form').style.display = 'block';
document.getElementById('event_title').value = info.event.title;
}",
dateClick: "function(info) {
document.getElementById('event-form').style.display = 'block';
document.getElementById('event_start').value = info.dateStr;
}"
}}
/>
</div>
<div class="col-md-4">
<div id="event-form" style="display: none;">
<.simple_form for={@changeset} action={Routes.event_path(@conn, :create)}>
<.input field={@changeset[:title]} label="Title" id="event_title" />
<.input field={@changeset[:start]} label="Start Date" type="date" id="event_start" />
<:actions>
<.button>Create Event</.button>
</:actions>
</.simple_form>
</div>
</div>
</div>
```
## Acknowledgments
This library builds on EventCalendar by Vlad Kurko: https://github.com/vkurko/calendar/
Thanks to the EventCalendar project for providing a lightweight, flexible calendar core.
## License
This project is licensed under the MIT License. See the `LICENSE` file for details.