CalendarComponent

Hex.pmDocs

Phoenix LiveView component library that renders an interactive calendar powered by EventCalendar. It ships as a library (not a full Phoenix app) with secure colocated JS/CSS assets.

Key Features:

Installation

Add calendar_component to your list of dependencies in mix.exs:

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

JavaScript Setup

For LiveView Applications

In your assets/js/app.js:

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 Phoenix Controllers

In your assets/js/app.js:

import { initStaticCalendars } from "calendar_component/static"

// Initialize static calendars when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
  initStaticCalendars()
})

Phoenix LiveView 1.8+ JS Commands Support

This library fully supports Phoenix.LiveView.JS commands for rich client-side interactions:

alias Phoenix.LiveView.JS

~H"""
<.calendar
  id="calendar"
  events={@events}
  on_event_click={JS.push("select_event") |> JS.show(to: "#event-modal")}
  on_date_click={JS.push("create_event") |> JS.add_class("highlight", to: ".selected")}
  on_month_change={JS.push("month_changed") |> JS.dispatch("calendar:month_changed")}
/>
"""

The JS commands execute on the client immediately, then server events are handled for persistence and state management.

Usage

With LiveView

Basic calendar with event handling:

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:

@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}

@impl true def handle_event("month_changed", %{"month" => month, "year" => year}, socket), do: {:noreply, socket}


### With Regular Phoenix Controllers (Static Calendar)

For traditional Phoenix controllers (without LiveView), use the secure static calendar component:

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, events: events)

end end

In your template (.html.heex) - Using secure global function references

<.static_calendar id="my-calendar" events={@events} options={%{

view: "dayGridMonth",
eventClick: "MyApp.Calendar.handleEventClick",
dateClick: "MyApp.Calendar.handleDateClick",
datesSet: "MyApp.Calendar.handleMonthChange"

}} />

// In your app.js - Define global callback functions import { initStaticCalendars } from "calendar_component/static";

window.MyApp = { Calendar: {

handleEventClick(info) {
  console.log('Event clicked:', info.event.title);
  alert('Event: ' + info.event.title);
  // Add your event handling logic here
},

handleDateClick(info) {
  console.log('Date clicked:', info.dateStr);
  // Redirect to create new event
  window.location.href = `/events/new?date=${info.dateStr}`;
},

handleMonthChange(info) {
  console.log('Month changed to:', info.start);
  // Update URL or fetch month-specific data
}

} };

document.addEventListener('DOMContentLoaded', () => initStaticCalendars());


#### Static Calendar Security Features

**✅ Secure Global Function References (Recommended):**

options: %{eventClick: "MyApp.handleEvent"}


**⚠️ Limited Function Body Strings (For Simple Cases Only):**

options: %{eventClick: "console.log('Event:', info.event.title)"}


**🔒 Security Measures Applied:**
- Function strings limited to 200 characters max
- Dangerous keywords blocked: `eval`, `Function`, `script`, `innerHTML`, `document.write`, etc.
- Complex logic should use global function references for security
- All callbacks are sanitized and validated

**See our [Security Documentation](docs/security.md) for complete guidelines.**

## Component API Reference

### LiveView Calendar Component

<.calendar id="calendar" # Required: unique DOM ID events={@events} # List of event maps on_event_click={JS.push(...)} # Phoenix.LiveView.JS commands for event clicks on_date_click={JS.push(...)} # Phoenix.LiveView.JS commands for date clicks on_month_change={JS.push(...)} # Phoenix.LiveView.JS commands for month navigation options={%{...}} # EventCalendar options (optional) rest={@rest} # Global attributes (phx-, data-, aria-*, class, style) />


### Static Calendar Component

<.static_calendar id="calendar" # Required: unique DOM ID events={@events} # List of event maps options={%{ # EventCalendar options with secure callbacks

view: "dayGridMonth",
eventClick: "MyApp.handleEvent",     # Global function reference (secure)
dateClick: "console.log('clicked')"  # Simple expression (limited, validated)

}} rest={@rest} # Global attributes />


### Event Data Structure

Events should be maps with EventCalendar-compatible properties:

%{ id: 1, # Unique identifier title: "Meeting", # Event title start: "2025-08-01T09:00:00", # ISO datetime or date string end: "2025-08-01T10:00:00", # Optional end time allDay: true, # Optional all-day flag color: "#FE6B64", # Optional color resourceId: 1, # Optional for resource views display: "background" # Optional display type }


### Options Mapping

Pass any EventCalendar options via the `:options` assign. They are forwarded to the JavaScript calendar:

- **View Types**: `dayGridMonth`, `timeGridWeek`, `timeGridDay`, `listWeek`, `resourceTimeGridWeek`, `resourceTimelineWeek`
- **Header Toolbar**: Customize buttons and title positioning
- **Resource Views**: Full support for resource timeline calendars
- **Theming**: Light and dark theme support
- **Event Rendering**: Colors, display modes, background events
- **Interaction**: Selectable dates, drag-and-drop, resizing

For complete options reference, see [EventCalendar Documentation](docs/event_calendar.md).

In your template:

# events/index.html.heex
<.static_calendar
  id="static-calendar"
  events={@events}
  options={%{
    view: "dayGridMonth",
    eventClick: "function(info) {
      alert(&#39;Event: &#39; + info.event.title);
    }",
    dateClick: "function(info) {
      console.log(&#39;Date clicked:&#39;, 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):

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

const Hooks = window.LiveCalendarHooks || {}
const liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks })
liveSocket.connect()

Important note:

Starting with version 0.2.0, the library includes full support for resource views (resourceTimeGridWeek, resourceTimelineWeek) with proper event validation and error handling. CSS is automatically included when you import the JavaScript component.

For resource views, make sure to:

  1. Define options.resources with valid resource objects
  2. Assign resourceId to events that matches existing resource IDs
  3. Use valid ISO date strings for event start/end times

If you still have CSS issues, you can manually import it in your assets/css/app.css:

/* In assets/css/app.css - Only if necessary */
@import "../../deps/calendar_component/priv/static/assets/main.css";
```### Options mapping

- Pass any EventCalendar options via the `:options` assign of `<.calendar ... />`. They are forwarded to the JS calendar.
## Phoenix Examples

### LiveView Examples

#### 1) Basic calendar with events

defmodule MyAppWeb.CalendarLive do use MyAppWeb, :live_view alias Phoenix.LiveView.JS

def render(assigns) do

~H"""
<div class="calendar-container">
  <h1>My Calendar</h1>
  <.calendar
    id="basic-calendar"
    events={@events}
    on_event_click={JS.push("event_selected")}
  />
</div>
"""

end

def mount(_params, _session, socket) do

events = [
  %{id: 1, title: "Team Meeting", start: "2025-08-01T09:00:00", end: "2025-08-01T10:00:00"},
  %{id: 2, title: "Project Demo", start: "2025-08-02T14:00:00", color: "#FE6B64"},
  %{id: 3, title: "All-day Event", start: "2025-08-03", allDay: true, color: "#B29DD9"}
]
{:ok, assign(socket, events: events)}

end

@impl true def handle_event("event_selected", %{"id" => id}, socket) do

{:noreply, put_flash(socket, :info, "Selected event: #{id}")}

end end


#### 2) Interactive calendar with rich JS commands

defmodule MyAppWeb.InteractiveCalendarLive do use MyAppWeb, :live_view alias Phoenix.LiveView.JS

def render(assigns) do

~H"""
<div>
  <.calendar
    id="interactive-calendar"
    events={@events}
    on_event_click={
      JS.push("event_clicked")
      |> JS.show(to: "#event-details")
      |> JS.add_class("highlight", to: ".selected-event")
    }
    on_date_click={
      JS.push("date_clicked")
      |> JS.show(to: "#new-event-form")
      |> JS.focus(to: "#event-title")
    }
    on_month_change={
      JS.push("month_changed")
      |> JS.dispatch("calendar:month_changed")
    }
    options={%{
      view: "dayGridMonth",
      selectable: true,
      nowIndicator: true,
      height: "600px"
    }}
  />

  <div id="event-details" style="display: none;" class="mt-4 p-4 bg-blue-100 rounded">
    <h3>Event Details</h3>
    <p>Selected event: <%= @selected_event_title %></p>
  </div>

  <div id="new-event-form" style="display: none;" class="mt-4 p-4 bg-green-100 rounded">
    <h3>Create New Event</h3>
    <input id="event-title" type="text" placeholder="Event title..." class="border p-2 rounded">
  </div>
</div>
"""

end

def mount(_params, _session, socket) do

events = create_sample_events()
{:ok, assign(socket, events: events, selected_event_title: nil)}

end

@impl true def handle_event("event_clicked", %{"id" => id, "title" => title}, socket) do

{:noreply, assign(socket, selected_event_title: title)}

end

@impl true def handle_event("date_clicked", %{"date" => date}, socket) do

new_event = %{
  id: System.unique_integer([:positive]),
  title: "New Event",
  start: date,
  color: "#779ECB"
}
events = [new_event | socket.assigns.events]
{:noreply, assign(socket, events: events)}

end

@impl true def handle_event("month_changed", %{"month" => month, "year" => year}, socket) do

{:noreply, put_flash(socket, :info, "Viewing #{month}/#{year}")}

end

defp create_sample_events do

today = Date.utc_today()
[
  %{
    id: 1,
    title: "Morning Meeting",
    start: "#{today}T09:00:00",
    end: "#{today}T10:00:00",
    color: "#FE6B64"
  },
  %{
    id: 2,
    title: "Lunch Break",
    start: "#{Date.add(today, 1)}T12:00:00",
    end: "#{Date.add(today, 1)}T13:00:00",
    color: "#B29DD9"
  }
]

end end


#### 3) Advanced resource calendar

def render(assigns) do ~H""" <.calendar

id="resource-calendar"
events={@events}
on_event_click={JS.push("resource_event_clicked")}
options={%{
  view: "resourceTimeGridWeek",
  resources: [
    %{id: 1, title: "Conference Room A"},
    %{id: 2, title: "Conference Room B"},
    %{id: 3, title: "Meeting Room C"}
  ],
  headerToolbar: %{
    start: "prev,next today",
    center: "title",
    end: "resourceTimeGridWeek,resourceTimelineWeek,dayGridMonth"
  },
  scrollTime: "08:00:00",
  slotMinTime: "08:00:00",
  slotMaxTime: "20:00:00",
  nowIndicator: true
}}

/> """ end

defp create_resource_events do [

%{
  id: 1,
  title: "Team Meeting",
  start: "2025-08-15T10:00:00",
  end: "2025-08-15T11:00:00",
  resourceId: 1,
  color: "#FE6B64"
},
%{
  id: 2,
  title: "Workshop",
  start: "2025-08-15T14:00:00",
  end: "2025-08-15T16:00:00",
  resourceId: 2,
  color: "#779ECB"
}

] end


#### 4) Drag and Drop Calendar with Event Management

defmodule MyAppWeb.DragDropCalendarLive do use MyAppWeb, :live_view alias Phoenix.LiveView.JS

def render(assigns) do

~H"""
<div class="drag-drop-calendar">
  <h1>Drag & Drop Calendar</h1>

  <div class="alert alert-info" :if={@drop_message}>
    <%= @drop_message %>
  </div>

  <.calendar
    id="dragdrop-calendar"
    events={@events}
    on_event_click={
      JS.push("event_selected")
      |> JS.add_class("ring-2 ring-blue-500", to: ".selected-event")
    }
    options={%{
      view: "timeGridWeek",
      editable: true,                    # Enable drag and drop
      droppable: true,                   # Allow external drops
      selectable: true,                  # Allow date selection
      selectMirror: true,                # Show selection mirror
      eventStartEditable: true,          # Allow event start time editing
      eventDurationEditable: true,       # Allow event duration editing
      eventResourceEditable: true,       # Allow resource changes (if using resources)
      dayMaxEvents: true,                # Limit events per day
      height: "600px",
      scrollTime: "08:00:00",
      slotMinTime: "07:00:00",
      slotMaxTime: "22:00:00",
      headerToolbar: %{
        start: "prev,next today",
        center: "title",
        end: "dayGridMonth,timeGridWeek,timeGridDay"
      },
      # Drag and drop event handlers using Phoenix LiveView hooks
      eventDrop: "handleEventDrop",      # Custom hook function
      eventResize: "handleEventResize",  # Custom hook function
      drop: "handleExternalDrop",        # External drop handler
      eventReceive: "handleEventReceive" # Event received from external
    }}
  />
  <div class="mt-6 p-4 bg-gray-100 rounded">
    <h3 class="font-semibold mb-3">Drag these events to the calendar:</h3>
    <div class="space-y-2">
      <div
        class="external-event bg-blue-500 text-white p-2 rounded cursor-move"
        data-event='{"title": "New Meeting", "duration": "01:00", "color": "#3B82F6"}'
      >
        📅 New Meeting (1 hour)
      </div>
      <div
        class="external-event bg-green-500 text-white p-2 rounded cursor-move"
        data-event='{"title": "Team Standup", "duration": "00:30", "color": "#10B981"}'
      >
        🤝 Team Standup (30 min)
      </div>
      <div
        class="external-event bg-purple-500 text-white p-2 rounded cursor-move"
        data-event='{"title": "Code Review", "duration": "02:00", "color": "#8B5CF6"}'
      >
        💻 Code Review (2 hours)
      </div>
    </div>
  </div>
</div>
"""

end

def mount(_params, _session, socket) do

events = create_draggable_events()
{:ok, assign(socket, events: events, drop_message: nil)}

end

# Handle when an existing event is moved (drag and drop) @impl true def handle_event("event_dropped", params, socket) do

%{
  "id" => id,
  "start" => new_start,
  "end" => new_end,
  "resourceId" => resource_id
} = params

# Update the event in your data store
updated_events =
  Enum.map(socket.assigns.events, fn event ->
    if event.id == String.to_integer(id) do
      event
      |> Map.put(:start, new_start)
      |> Map.put(:end, new_end)
      |> maybe_put_resource(resource_id)
    else
      event
    end
  end)

message = "Event moved to #{format_datetime(new_start)}"

{:noreply, assign(socket, events: updated_events, drop_message: message)}

end

# Handle when an event is resized @impl true def handle_event("event_resized", params, socket) do

%{
  "id" => id,
  "start" => new_start,
  "end" => new_end
} = params

updated_events =
  Enum.map(socket.assigns.events, fn event ->
    if event.id == String.to_integer(id) do
      event
      |> Map.put(:start, new_start)
      |> Map.put(:end, new_end)
    else
      event
    end
  end)

message = "Event resized: #{format_datetime(new_start)} - #{format_datetime(new_end)}"

{:noreply, assign(socket, events: updated_events, drop_message: message)}

end

# Handle when an external event is dropped onto the calendar @impl true def handle_event("external_dropped", params, socket) do

%{
  "title" => title,
  "start" => start_time,
  "end" => end_time,
  "color" => color
} = params

new_event = %{
  id: System.unique_integer([:positive]),
  title: title,
  start: start_time,
  end: end_time,
  color: color
}

updated_events = [new_event | socket.assigns.events]
message = "New event '#{title}' added at #{format_datetime(start_time)}"

{:noreply, assign(socket, events: updated_events, drop_message: message)}

end

@impl true def handle_event("event_selected", %{"id" => id}, socket) do

{:noreply, put_flash(socket, :info, "Selected event: #{id}")}

end

# Helper functions defp create_draggable_events do

today = Date.utc_today()
[
  %{
    id: 1,
    title: "Daily Standup",
    start: "#{today}T09:00:00",
    end: "#{today}T09:30:00",
    color: "#10B981"
  },
  %{
    id: 2,
    title: "Sprint Planning",
    start: "#{today}T10:00:00",
    end: "#{today}T12:00:00",
    color: "#3B82F6"
  },
  %{
    id: 3,
    title: "Lunch Break",
    start: "#{Date.add(today, 1)}T12:00:00",
    end: "#{Date.add(today, 1)}T13:00:00",
    color: "#F59E0B"
  }
]

end

defp maybe_put_resource(event, nil), do: event defp maybe_put_resource(event, ""), do: event defp maybe_put_resource(event, resource_id), do: Map.put(event, :resourceId, String.to_integer(resource_id))

defp format_datetime(datetime_str) do

case DateTime.from_iso8601(datetime_str) do
  {:ok, dt, _} -> Calendar.strftime(dt, "%B %d at %I:%M %p")
  _ -> datetime_str
end

end end


You'll also need to add custom JavaScript hooks for drag and drop handling:

// In your assets/js/app.js - Add these custom hooks let Hooks = {}

// Hook for handling drag and drop events in LiveView calendars Hooks.LiveCalendar = { mounted() {

this.handleCalendarEvents()

},

updated() {

this.handleCalendarEvents()

},

handleCalendarEvents() {

// Add custom drag and drop event handlers
if (this.calendar) {
  this.calendar.setOption('eventDrop', (info) => {
    this.pushEvent("event_dropped", {
      id: info.event.id,
      start: info.event.start.toISOString(),
      end: info.event.end ? info.event.end.toISOString() : info.event.start.toISOString(),
      resourceId: info.newResource ? info.newResource.id : null
    })
  })

  this.calendar.setOption('eventResize', (info) => {
    this.pushEvent("event_resized", {
      id: info.event.id,
      start: info.event.start.toISOString(),
      end: info.event.end.toISOString()
    })
  })

  this.calendar.setOption('drop', (info) => {
    const eventData = JSON.parse(info.draggedEl.dataset.event)
    const endTime = new Date(info.date.getTime() + (parseInt(eventData.duration.split(':')[0]) * 60 * 60 * 1000) + (parseInt(eventData.duration.split(':')[1]) * 60 * 1000))

    this.pushEvent("external_dropped", {
      title: eventData.title,
      start: info.date.toISOString(),
      end: endTime.toISOString(),
      color: eventData.color
    })

    // Remove the dragged element
    info.draggedEl.remove()
  })
}

} }

// Initialize external draggable events document.addEventListener('DOMContentLoaded', function() { const draggableElements = document.querySelectorAll('.external-event') draggableElements.forEach(el => {

new Draggable(el, {
  itemSelector: '.external-event',
  data: el.dataset.event
})

}) })

let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks })


### Controller Examples (Static Calendar)

#### 1) Basic static calendar with secure callbacks

Controller

defmodule MyAppWeb.EventController do use MyAppWeb, :controller import LiveCalendar.Components

def index(conn, _params) do

events = [
  %{id: 1, title: "Team Meeting", start: "2025-08-15T10:00:00", color: "#FE6B64"},
  %{id: 2, title: "Project Review", start: "2025-08-16T14:00:00", color: "#B29DD9"},
  %{id: 3, title: "All-day Event", start: "2025-08-17", allDay: true, color: "#779ECB"}
]
render(conn, "index.html", events: events)

end end

Template: events/index.html.heex

My Events

<.static_calendar id="events-calendar" events={@events} options={%{ view: "dayGridMonth", height: "600px", nowIndicator: true, eventClick: "MyApp.Events.handleEventClick", dateClick: "MyApp.Events.handleDateClick" }} />

// In your app.js - Define secure global callbacks import { initStaticCalendars } from "calendar_component/static";

window.MyApp = { Events: {

handleEventClick(info) {
  // Secure: redirect to event details
  window.location.href = `/events/${info.event.id}`;
},

handleDateClick(info) {
  // Secure: redirect to create new event
  window.location.href = `/events/new?date=${info.dateStr}`;
}

} };

document.addEventListener('DOMContentLoaded', () => initStaticCalendars());


#### 2) Interactive static calendar with form integration

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

<.static_calendar id="calendar-with-form" events={@events} options={%{ view: "dayGridMonth", selectable: true, eventClick: "MyApp.Calendar.showEventDetails", dateClick: "MyApp.Calendar.showNewEventForm", select: "MyApp.Calendar.handleDateRange" }} />

<div class="col-md-4">

<div id="event-form" style="display: none;" class="card">
  <div class="card-header">
    <h5>Create New Event</h5>
  </div>
  <div class="card-body">
    <.simple_form for={@changeset} action={Routes.event_path(@conn, :create)}>
      <.input field={@changeset[:title]} label="Title" id="event_title" />
      <.input field={@changeset[:start_date]} label="Start Date" type="date" id="event_start_date" />
      <.input field={@changeset[:start_time]} label="Start Time" type="time" id="event_start_time" />
      <.input field={@changeset[:description]} label="Description" type="textarea" />
      <:actions>
        <.button type="submit">Create Event</.button>
        <button type="button" onclick="MyApp.Calendar.hideForm()">Cancel</button>
      </:actions>
    </.simple_form>
  </div>
</div>

// Secure JavaScript handlers window.MyApp = { Calendar: {

showEventDetails(info) {
  alert(`Event: ${info.event.title}\nTime: ${info.event.start}`);
  // Or show in modal/sidebar
},

showNewEventForm(info) {
  const form = document.getElementById('event-form');
  const dateInput = document.getElementById('event_start_date');
  const timeInput = document.getElementById('event_start_time');

  form.style.display = 'block';
  dateInput.value = info.dateStr;

  // If time is available, set it
  if (info.date) {
    const hours = info.date.getHours().toString().padStart(2, '0');
    const minutes = info.date.getMinutes().toString().padStart(2, '0');
    timeInput.value = `${hours}:${minutes}`;
  }

  document.getElementById('event_title').focus();
},

handleDateRange(info) {
  const title = prompt('Event title for selected time range:');
  if (title) {
    // Create event via form submission or AJAX
    const form = document.querySelector('#event-form form');
    document.getElementById('event_title').value = title;
    document.getElementById('event_start_date').value = info.startStr.split('T')[0];
    form.submit();
  }
},

hideForm() {
  document.getElementById('event-form').style.display = 'none';
}

} };


#### 3) Advanced resource timeline calendar

def show(conn, _params) do events = create_weekly_events() resources = create_resources()

render(conn, "show.html", events: events, resources: resources) end

defp create_resources do [

%{id: 1, title: "Conference Room A"},
%{id: 2, title: "Conference Room B"},
%{id: 3, title: "Meeting Room C"},
%{
  id: 4,
  title: "Building Floor 2",
  children: [
    %{id: 5, title: "Room 2A"},
    %{id: 6, title: "Room 2B"}
  ]
}

] end

Template: events/show.html.heex

<.static_calendar id="resource-timeline-calendar" events={@events} options={%{

view: "resourceTimelineWeek",
headerToolbar: %{
  start: "prev,next today",
  center: "title",
  end: "dayGridMonth,timeGridWeek,resourceTimeGridWeek,resourceTimelineWeek"
},
resources: @resources,
scrollTime: "09:00:00",
views: %{
  resourceTimelineWeek: %{
    slotDuration: "00:15",
    slotLabelInterval: "01:00",
    slotMinTime: "08:00",
    slotMaxTime: "20:00"
  }
},
dayMaxEvents: true,
nowIndicator: true,
selectable: true,
eventClick: "MyApp.Resources.handleResourceEvent"

}} />

// Resource calendar handlers
window.MyApp = {
  Resources: {
    handleResourceEvent(info) {
      const resource = info.event.getResources()[0];
      console.log(&#39;Event:&#39;, info.event.title, &#39;Resource:&#39;, resource?.title);

      // Show resource-specific actions
      if (confirm(`Manage "${info.event.title}" in ${resource?.title}?`)) {
        window.location.href = `/resources/${resource.id}/events/${info.event.id}`;
      }
    }
  }
};

4) Drag and Drop Static Calendar

# Controller
defmodule MyAppWeb.DragDropController do
  use MyAppWeb, :controller
  import LiveCalendar.Components

  def index(conn, _params) do
    events = create_draggable_events()
    render(conn, "index.html", events: events)
  end

  def update_event(conn, %{"id" => id} = params) do
    # Handle AJAX request to update event after drag/drop
    case Events.update_event(id, params) do
      {:ok, _event} ->
        conn
        |> put_status(200)
        |> json(%{success: true, message: "Event updated successfully"})

      {:error, _} ->
        conn
        |> put_status(400)
        |> json(%{success: false, message: "Failed to update event"})
    end
  end

  def create_event(conn, params) do
    # Handle AJAX request to create new event from external drop
    case Events.create_event(params) do
      {:ok, event} ->
        conn
        |> put_status(201)
        |> json(%{success: true, event: event, message: "Event created successfully"})

      {:error, changeset} ->
        conn
        |> put_status(400)
        |> json(%{success: false, errors: translate_errors(changeset)})
    end
  end

  defp create_draggable_events do
    today = Date.utc_today()
    [
      %{
        id: 1,
        title: "Morning Standup",
        start: "#{today}T09:00:00",
        end: "#{today}T09:30:00",
        color: "#10B981",
        editable: true
      },
      %{
        id: 2,
        title: "Project Review",
        start: "#{today}T14:00:00",
        end: "#{today}T16:00:00",
        color: "#3B82F6",
        editable: true
      },
      %{
        id: 3,
        title: "Team Lunch",
        start: "#{Date.add(today, 1)}T12:00:00",
        end: "#{Date.add(today, 1)}T13:00:00",
        color: "#F59E0B",
        editable: false  # Not draggable
      }
    ]
  end
end
# Template: drag_drop/index.html.heex
<div class="drag-drop-container">
  <div class="row">
<!-- External draggable events -->

    <div class="col-md-3">
      <div class="external-events p-4 bg-gray-100 rounded">
        <h4 class="font-semibold mb-3">Drag Events to Calendar</h4>
        <p class="text-sm text-gray-600 mb-4">Drag these items onto the calendar to create new events</p>

        <div class="space-y-2">
          <div
            class="external-event bg-blue-500 text-white p-2 rounded cursor-move hover:bg-blue-600"
            data-event=&#39;{"title": "Quick Meeting", "duration": "00:30", "color": "#3B82F6"}&#39;
          >
            📅 Quick Meeting (30min)
          </div>

          <div
            class="external-event bg-green-500 text-white p-2 rounded cursor-move hover:bg-green-600"
            data-event=&#39;{"title": "Code Review", "duration": "01:00", "color": "#10B981"}&#39;
          >
            🔍 Code Review (1hr)
          </div>

          <div
            class="external-event bg-purple-500 text-white p-2 rounded cursor-move hover:bg-purple-600"
            data-event=&#39;{"title": "Design Session", "duration": "02:00", "color": "#8B5CF6"}&#39;
          >
            🎨 Design Session (2hrs)
          </div>

          <div
            class="external-event bg-red-500 text-white p-2 rounded cursor-move hover:bg-red-600"
            data-event=&#39;{"title": "Client Call", "duration": "00:45", "color": "#EF4444"}&#39;
          >
            📞 Client Call (45min)
          </div>
        </div>
      </div>

<!-- Status messages -->

      <div id="drag-messages" class="mt-4">
        <div id="success-message" class="alert alert-success" style="display: none;"></div>
        <div id="error-message" class="alert alert-danger" style="display: none;"></div>
      </div>
    </div>

<!-- Calendar -->

    <div class="col-md-9">
      <.static_calendar
        id="dragdrop-static-calendar"
        events={@events}
        options={%{
          view: "timeGridWeek",
          height: "700px",
          editable: true,                    # Enable drag and drop for existing events
          droppable: true,                   # Allow external drops
          selectable: true,                  # Allow date/time selection
          selectMirror: true,                # Show selection feedback
          eventStartEditable: true,          # Allow changing event start time
          eventDurationEditable: true,       # Allow resizing events
          dayMaxEvents: true,                # Auto-limit events per day in month view
          scrollTime: "08:00:00",
          slotMinTime: "07:00:00",
          slotMaxTime: "22:00:00",
          headerToolbar: %{
            start: "prev,next today",
            center: "title",
            end: "dayGridMonth,timeGridWeek,timeGridDay"
          },
          businessHours: %{              # Highlight business hours
            daysOfWeek: [1, 2, 3, 4, 5], # Monday - Friday
            startTime: "09:00",
            endTime: "17:00"
          },
          # Secure callback functions for drag and drop
          eventDrop: "MyApp.DragDrop.handleEventDrop",
          eventResize: "MyApp.DragDrop.handleEventResize",
          drop: "MyApp.DragDrop.handleExternalDrop",
          eventClick: "MyApp.DragDrop.handleEventClick",
          select: "MyApp.DragDrop.handleDateSelect"
        }}
      />
    </div>
  </div>
</div>
// In your app.js - Drag and drop handlers for static calendar
import { initStaticCalendars } from "calendar_component/static";

window.MyApp = {
  DragDrop: {
    // Handle when existing event is moved
    handleEventDrop(info) {
      const eventData = {
        id: info.event.id,
        start: info.event.start.toISOString(),
        end: info.event.end ? info.event.end.toISOString() : info.event.start.toISOString()
      };

      // Send AJAX request to update event
      fetch(`/events/${info.event.id}`, {
        method: &#39;PATCH&#39;,
        headers: {
          &#39;Content-Type&#39;: &#39;application/json&#39;,
          &#39;X-CSRF-Token&#39;: document.querySelector(&#39;meta[name="csrf-token"]&#39;).content
        },
        body: JSON.stringify(eventData)
      })
      .then(response => response.json())
      .then(data => {
        if (data.success) {
          this.showMessage(&#39;success&#39;, `Event "${info.event.title}" moved successfully!`);
        } else {
          this.showMessage(&#39;error&#39;, &#39;Failed to move event. Please try again.&#39;);
          info.revert(); // Revert the move if server update failed
        }
      })
      .catch(error => {
        console.error(&#39;Error updating event:&#39;, error);
        this.showMessage(&#39;error&#39;, &#39;Network error. Please try again.&#39;);
        info.revert();
      });
    },

    // Handle when event is resized
    handleEventResize(info) {
      const eventData = {
        id: info.event.id,
        start: info.event.start.toISOString(),
        end: info.event.end.toISOString()
      };

      fetch(`/events/${info.event.id}`, {
        method: &#39;PATCH&#39;,
        headers: {
          &#39;Content-Type&#39;: &#39;application/json&#39;,
          &#39;X-CSRF-Token&#39;: document.querySelector(&#39;meta[name="csrf-token"]&#39;).content
        },
        body: JSON.stringify(eventData)
      })
      .then(response => response.json())
      .then(data => {
        if (data.success) {
          this.showMessage(&#39;success&#39;, `Event "${info.event.title}" resized successfully!`);
        } else {
          this.showMessage(&#39;error&#39;, &#39;Failed to resize event. Please try again.&#39;);
          info.revert();
        }
      })
      .catch(error => {
        console.error(&#39;Error resizing event:&#39;, error);
        this.showMessage(&#39;error&#39;, &#39;Network error. Please try again.&#39;);
        info.revert();
      });
    },

    // Handle external event drop (create new event)
    handleExternalDrop(info) {
      try {
        const eventData = JSON.parse(info.draggedEl.dataset.event);
        const duration = eventData.duration.split(&#39;:&#39;);
        const endTime = new Date(info.date.getTime() +
          (parseInt(duration[0]) * 60 * 60 * 1000) +
          (parseInt(duration[1]) * 60 * 1000));

        const newEventData = {
          title: eventData.title,
          start: info.date.toISOString(),
          end: endTime.toISOString(),
          color: eventData.color
        };

        // Create new event via AJAX
        fetch(&#39;/events&#39;, {
          method: &#39;POST&#39;,
          headers: {
            &#39;Content-Type&#39;: &#39;application/json&#39;,
            &#39;X-CSRF-Token&#39;: document.querySelector(&#39;meta[name="csrf-token"]&#39;).content
          },
          body: JSON.stringify(newEventData)
        })
        .then(response => response.json())
        .then(data => {
          if (data.success) {
            this.showMessage(&#39;success&#39;, `Event "${eventData.title}" created successfully!`);
            // Remove the dragged element from external events
            info.draggedEl.remove();
          } else {
            this.showMessage(&#39;error&#39;, &#39;Failed to create event. Please try again.&#39;);
          }
        })
        .catch(error => {
          console.error(&#39;Error creating event:&#39;, error);
          this.showMessage(&#39;error&#39;, &#39;Network error. Please try again.&#39;);
        });
      } catch (error) {
        console.error(&#39;Error processing drop:&#39;, error);
        this.showMessage(&#39;error&#39;, &#39;Invalid event data. Please try again.&#39;);
      }
    },

    // Handle event click
    handleEventClick(info) {
      if (confirm(`Edit event: "${info.event.title}"?`)) {
        window.location.href = `/events/${info.event.id}/edit`;
      }
    },

    // Handle date/time selection
    handleDateSelect(info) {
      const title = prompt(&#39;Enter event title:&#39;);
      if (title && title.trim()) {
        const newEventData = {
          title: title.trim(),
          start: info.start.toISOString(),
          end: info.end.toISOString(),
          color: &#39;#3B82F6&#39;
        };

        fetch(&#39;/events&#39;, {
          method: &#39;POST&#39;,
          headers: {
            &#39;Content-Type&#39;: &#39;application/json&#39;,
            &#39;X-CSRF-Token&#39;: document.querySelector(&#39;meta[name="csrf-token"]&#39;).content
          },
          body: JSON.stringify(newEventData)
        })
        .then(response => response.json())
        .then(data => {
          if (data.success) {
            this.showMessage(&#39;success&#39;, `Event "${title}" created successfully!`);
            // Refresh the page to show the new event
            setTimeout(() => location.reload(), 1500);
          } else {
            this.showMessage(&#39;error&#39;, &#39;Failed to create event. Please try again.&#39;);
          }
        })
        .catch(error => {
          console.error(&#39;Error creating event:&#39;, error);
          this.showMessage(&#39;error&#39;, &#39;Network error. Please try again.&#39;);
        });
      }
    },

    // Utility function to show messages
    showMessage(type, message) {
      const messageDiv = document.getElementById(`${type}-message`);
      messageDiv.textContent = message;
      messageDiv.style.display = &#39;block&#39;;

      // Hide other message types
      const otherType = type === &#39;success&#39; ? &#39;error&#39; : &#39;success&#39;;
      document.getElementById(`${otherType}-message`).style.display = &#39;none&#39;;

      // Auto-hide after 5 seconds
      setTimeout(() => {
        messageDiv.style.display = &#39;none&#39;;
      }, 5000);
    }
  }
};

// Initialize external draggable events and static calendars
document.addEventListener(&#39;DOMContentLoaded&#39;, function() {
  // Initialize static calendars first
  initStaticCalendars();

  // Make external events draggable (requires a drag library like Draggable)
  const draggableEvents = document.querySelectorAll(&#39;.external-event&#39;);
  draggableEvents.forEach(eventEl => {
    eventEl.draggable = true;
    eventEl.addEventListener(&#39;dragstart&#39;, function(e) {
      // Store the event data for the drop handler
      e.dataTransfer.setData(&#39;text/plain&#39;, eventEl.dataset.event);
    });
  });
});

#### 4) Simple calendar with minimal callbacks

For simple use cases, you can use limited function body strings:

<.static_calendar id="simple-calendar" events={@events} options={%{

view: "dayGridMonth",
eventClick: "alert('Event: ' + info.event.title)",
dateClick: "console.log('Clicked:', info.dateStr)"

}} />


⚠️ **Security Note**: Function body strings are limited to 200 characters and dangerous keywords are blocked. For complex logic, always use global function references.

});

5) Simple calendar with minimal callbacks

For simple use cases, you can use limited function body strings:

<.static_calendar
  id="simple-calendar"
  events={@events}
  options={%{
    view: "dayGridMonth",
    eventClick: "alert(&#39;Event: &#39; + info.event.title)",
    dateClick: "console.log(&#39;Clicked:&#39;, info.dateStr)"
  }}
/>

⚠️ Security Note: Function body strings are limited to 200 characters and dangerous keywords are blocked. For complex logic like drag and drop, always use global function references.

Drag and Drop Best Practices

For LiveView Calendars:

For Static Calendars:

Security Considerations:

Performance Tips:

Important Notes

Version 0.2.0+ Features

Security First

All callback functions are validated and sanitized:

Migration from 0.1.x

If upgrading from version 0.1.x:

  1. Replace function body strings with global function references
  2. Update JavaScript imports to use new module structure
  3. Review callback implementations for security compliance
  4. Run tests to verify functionality

Additional Resources

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 that makes this Phoenix integration possible.

License

This project is licensed under the MIT License. See the LICENSE file for details.