Feature
Calendar Basic
A basic calendar build with Phoenix LiveView.
lib/phoenix_features_web/live/components/calendar_simple.ex
defmodule PhoenixFeaturesWeb.Components.CalendarSimple do
use PhoenixFeaturesWeb, :live_component
use Timex
alias PhoenixFeaturesWeb.Components.CalendarDay
@week_start_at :mon
def update(assigns, socket) do
current_date = assigns |> extract_from_params()
current_month = assigns |> extract_from_params()
{:ok,
socket
|> assign(assigns)
|> assign(:current_date, current_date)
|> assign(:current_month, current_month)
|> assign(:day_names, day_names(@week_start_at))
|> assign(:week_rows, week_rows(current_date))
}
end
def handle_event("prev-month", _, socket) do
current_month = Timex.shift(socket.assigns.current_month, months: -1)
{
:noreply,
socket
|> assign(:current_month, current_month)
|> assign(:week_rows, week_rows(current_month))
}
end
def handle_event("next-month", _, socket) do
current_month = Timex.shift(socket.assigns.current_month, months: 1)
{
:noreply,
socket
|> assign(:current_month, current_month)
|> assign(:week_rows, week_rows(current_month))
}
end
def handle_event("pick-date", %{"date" => date}, socket) do
current_date = Timex.parse!(date, "{YYYY}-{0M}-{D}")
{
:noreply,
socket
|> assign(:current_date, current_date)
}
end
defp extract_from_params(%{params: %{"date" => "" <> date}}), do: Timex.parse!(date, "{YYYY}-{0M}-{D}")
defp extract_from_params(_), do: Timex.now
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
lib/phoenix_features_web/live/components/calendar_simple.html.leex
<div id="<%= @id %>">
<div class="flex items-baseline justify-between">
<h3 class="ml-4 text-lg font-semibold text-gray-600">
<%= Timex.format!(@current_month, "%B %Y", :strftime) %>
</h3>
<div>
<a href="#" phx-target="#<%= @id %>" 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-target="#<%= @id %>" 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>
<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 %>
<%= live_component @socket, CalendarDay, id: day, parent_id: @id, day: day, current_month: @current_month, current_date: @current_date %>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
lib/phoenix_features_web/live/components/calendar_day.ex
defmodule PhoenixFeaturesWeb.Components.CalendarDay do
use PhoenixFeaturesWeb, :live_component
use Timex
def update(assigns, socket) do
{
:ok,
socket
|> assign(assigns)
|> assign(:day_class, day_class(assigns))
}
end
def render(assigns) do
~L"""
<td
id="td-<%= Timex.format!(@day, "%Y%m%d", :strftime) %>"
phx-target="#<%= @parent_id %>"
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_month, [:year, :month])
end
end
test/phoenix_features_web/live/demo_live_test.exs
defmodule PhoenixFeaturesWeb.Components.CalendarSimpleTest do
use PhoenixFeaturesWeb.ConnCase
import Phoenix.LiveViewTest
describe "CalendarSimple" do
test "shows the calendar", %{conn: conn} do
{:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "calendar_simple"))
assert view |> element("#calendar_simple") |> has_element?()
end
test "shows the calendar for a specific month and highlights selected day", %{conn: conn} do
{:ok, view, html} = live(conn, Routes.demos_show_path(conn, :show, "calendar_simple", date: "2021-01-23"))
assert html =~ "January 2021"
refute view |> has_element?("#td-20210122.bg-blue-100")
assert view |> has_element?("#td-20210123.bg-blue-100")
end
test "click next shows the calendar for next month", %{conn: conn} do
{:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "calendar_simple", date: "2021-01-23"))
assert view |> element("a", "Next") |> render_click() =~ "February 2021"
end
test "click previous shows the calendar for previous month", %{conn: conn} do
{:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "calendar_simple", date: "2021-01-23"))
assert view |> element("a", "Prev") |> render_click() =~ "December 2020"
end
test "click a date selects it", %{conn: conn} do
{:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "calendar_simple", date: "2021-01-23"))
refute view |> has_element?("#td-20210110.bg-blue-100")
assert view |> element("#td-20210110") |> render_click()
assert view |> has_element?("#td-20210110.bg-blue-100")
refute view |> has_element?("#td-20210123.bg-blue-100")
end
end
end