View on Github

Create a Calendar in Phoenix LiveView

This post was updated 01 May - 2020


In this tutorial I am creating a simple calendar in Phoenix LiveView. The calendar should be able to switch month, highlight current day and select a specific day. I might extend functionality in a future tutorial. And also, all styling are done with Tailwind.

And I do it in 163 lines of code and 0 lines of Javascript and 0 lines of CSS

STEP 1 - Install Timex

First off, I need to install the Timex library. It comes with some convenient helper functions that makes the rest of the work a lot easier.

# mix.exs
defp deps do
  [{:timex, "~> 3.5"}]

And then run:

mix deps.get

So, when that is installed, I can move on to the next part.

STEP 2 - Create Calendar component

Since I already know I want to do this in LiveView so I will go ahead and create a new component called TutorialWeb.CalendarLive.

The first step is so make it display current month and current date. I also want to make it configurable if the week starts with Sunday or Monday

# lib/tutorial_web/live/calendar_live.ex
defmodule TutorialWeb.CalendarLive do
  use Phoenix.LiveView
  use Timex

  @week_start_at :mon

  def mount(_session, socket) do
    current_date = Timex.now

    assigns = [
      conn: socket,
      current_date: current_date,
      day_names: day_names(@week_start_at),
      week_rows: week_rows(current_date)

    {:ok, assign(socket, assigns)}

  def render(assigns) do
    TutorialWeb.PageView.render("calendar.html", assigns)

  defp day_names(:sun), do:  [7, 1, 2, 3, 4, 5, 6] |> Enum.map(&Timex.day_shortname/1)
  defp day_names(_), do:  [1, 2, 3, 4, 5, 6, 7] |> Enum.map(&Timex.day_shortname/1)

  defp week_rows(current_date) do
    first =
      |> Timex.beginning_of_month()
      |> Timex.beginning_of_week(@week_start_at)

    last =
      |> Timex.end_of_month()
      |> Timex.end_of_week(@week_start_at)

    Interval.new(from: first, until: last)
    |> Enum.map(& &1)
    |> Enum.chunk_every(7)

The template file is pretty basic. I iterate over the list of day names to get the headers and then iterate first over the list of weeks, and then within a week row, I iterate over the actual days.

<!-- calendar.html.leex -->
<table class="w-full mt-4 border border-gray-200 rounded-lg shadow-lg">
<%= for day_name <- @day_names do %>
      <th class="text-xs p-2 text-gray-600 border border-gray-200">
        <%= day_name %>
<% end %>
<%= for week <- @week_rows do %>
  <%= for day <- week do %>
    <%= Timex.format!(day, "%d", :strftime) %>
  <% end %>
<% end %>

When that is done, I want to attach some interactivity to the calendar.

STEP 2 - Add month selection

The calendar are pretty useless without being able to switch months. So that would be the next thing to add. Open up the template again and in the top, add:

<!-- calendar.html.leex -->
<div class="flex items-baseline justify-between">
  <h3 class="ml-4 text-gray-600 text-lg">
    <%= Timex.format!(@current_date, "%B %Y", :strftime) %>
    <a href="#" phx-click="prev-month" class="inline-block text-sm bg-white p-2 rounded shadow text-gray-600 border border-gray-200">&laquo; Prev</a>
    <a href="#" phx-click="next-month" class="inline-block text-sm bg-white p-2 rounded shadow text-gray-600 border border-gray-200">&raquo; Next</a>

Note that I add phx-click="prev-month" to the links. So I also need to add handlers in the LiveView component.

So what I do is take current data and shift the month with either +1 or -1. Then I recalculate the weeks_rows based on the new date.

  # lib/tutorial_web/live/calendar_live.ex
  def handle_event("prev-month", _, socket) do
    current_date = Timex.shift(socket.assigns.current_date, months: -1)

    assigns = [
      current_date: current_date,
      week_rows: week_rows(current_date)

    {:noreply, assign(socket, assigns)}

  def handle_event("next-month", _, socket) do
    current_date = Timex.shift(socket.assigns.current_date, months: 1)

    assigns = [
      current_date: current_date,
      week_rows: week_rows(current_date)

    {:noreply, assign(socket, assigns)}

STEP 3 - Add functionality to a single day

Since the day cell will have more complex logic like:

  1. Display todays date with a specific color
  2. Display other months dates with a gray background
  3. Make a date selectable for future functionality
  4. Display events for this date (Not covered in this tutorial)

I think it make sense to extract it to its own LiveComponent.

# lib/tutorial_web/live/calendar_day_component.ex
defmodule TutorialWeb.CalendarDayComponent do
  use Phoenix.LiveComponent
  use Timex

  def render(assigns) do
    assigns = Map.put(assigns, :day_class, day_class(assigns))

    <td phx-click="pick-date" phx-value-date="<%= Timex.format!(@day, "%Y-%m-%d", :strftime) %>" class="<%= @day_class %>">
      <%= Timex.format!(@day, "%d", :strftime) %>

  defp day_class(assigns) do
    cond do
      today?(assigns) ->
        "text-xs p-2 text-gray-600 border border-gray-200 bg-green-200 hover:bg-green-300 cursor-pointer"
      current_date?(assigns) ->
        "text-xs p-2 text-gray-600 border border-gray-200 bg-blue-100 cursor-pointer"
      other_month?(assigns) ->
        "text-xs p-2 text-gray-400 border border-gray-200 bg-gray-200 cursor-not-allowed"
      true ->
        "text-xs p-2 text-gray-600 border border-gray-200 bg-white hover:bg-blue-100 cursor-pointer"

  defp current_date?(assigns) do
    Map.take(assigns.day, [:year, :month, :day]) == Map.take(assigns.current_date, [:year, :month, :day])

  defp today?(assigns) do
    Map.take(assigns.day, [:year, :month, :day]) == Map.take(Timex.now, [:year, :month, :day])

  defp other_month?(assigns) do
    Map.take(assigns.day, [:year, :month]) != Map.take(assigns.current_date, [:year, :month])

Note that I want the component, or day cell, to have different styling depending of what day it is. I also want to be able to click on a day to set it as active. I do that with phx-click="pick-date" and the value that are being sent is phx-value-date="<%= Timex.format!(@day, "%Y-%m-%d", :strftime) %>"

That makes is easy to parse in the LiveView Calendar component. And also, that is the last thing to add. Open CalendarLive again and add:

  # lib/tutorial_web/live/calendar_live.ex
  def handle_event("pick-date", %{"date" => date}, socket) do
    current_date = Timex.parse!(date, "{YYYY}-{0M}-{D}")

    assigns = [
      current_date: current_date

    {:noreply, assign(socket, assigns)}

So that is that. If you have any questions, you can reach out to me on Twitter and I will be happy to answer them.

Phoenix Bolerplate

Generate a Phoenix Boilerplate and save hours on your next project.

Try now

SAAS Starter Kit

Get started and save time and resources by using the SAAS Starter Kit built with Phoenix and LiveView.

Subscribe for $39/mo to geat ahead!

Learn More

Related Tutorials

Published 04 May - 2021 - Updated 05 May

How to combine Phoenix LiveView with Alpine.js

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..

Published 23 Dec - 2020

Getting Started with Phoenix and LiveView

This guide to getting started with Phoenix covers getting up and running with Elixir and Phoenix. This is a direct conversion of the Getting started..