Premium Tutorial. Learn Stripe Subscription with Phoenix LiveView
Read moreTutorial
View on GithubCreate 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"}]
end
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)}
end
def render(assigns) do
TutorialWeb.PageView.render("calendar.html", assigns)
end
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 =
current_date
|> Timex.beginning_of_month()
|> Timex.beginning_of_week(@week_start_at)
last =
current_date
|> Timex.end_of_month()
|> Timex.end_of_week(@week_start_at)
Interval.new(from: first, until: last)
|> Enum.map(& &1)
|> Enum.chunk_every(7)
end
end
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">
<thead>
<tr>
<%= for day_name <- @day_names do %>
<th class="text-xs p-2 text-gray-600 border border-gray-200">
<%= day_name %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<%= for week <- @week_rows do %>
<tr>
<%= for day <- week do %>
<%= Timex.format!(day, "%d", :strftime) %>
<% end %>
</tr>
<% end %>
</tbody>
</table>
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) %>
</h3>
<div>
<a href="#" phx-click="prev-month" class="inline-block text-sm bg-white p-2 rounded shadow text-gray-600 border border-gray-200">« 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">» Next</a>
</div>
</div>
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)}
end
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)}
end
STEP 3 - Add functionality to a single day
Since the day cell will have more complex logic like:
- Display todays date with a specific color
- Display other months dates with a gray background
- Make a date selectable for future functionality
- 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))
~L"""
<td phx-click="pick-date" phx-value-date="<%= Timex.format!(@day, "%Y-%m-%d", :strftime) %>" class="<%= @day_class %>">
<%= Timex.format!(@day, "%d", :strftime) %>
</td>
"""
end
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"
end
end
defp current_date?(assigns) do
Map.take(assigns.day, [:year, :month, :day]) == Map.take(assigns.current_date, [:year, :month, :day])
end
defp today?(assigns) do
Map.take(assigns.day, [:year, :month, :day]) == Map.take(Timex.now, [:year, :month, :day])
end
defp other_month?(assigns) do
Map.take(assigns.day, [:year, :month]) != Map.take(assigns.current_date, [:year, :month])
end
end
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)}
end
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.
What are you working on?
If you want, you can send me a link to your Phoenix or Phoenix LiveView project so. Lets connect on Twitter or Linkedin.
- Andreas Eriksson, web developer since 2005