Helpers for a LiveView (or a LiveComponent) with Koyomi pickers.
Every push carries the picker's id (the id of the component), so
that only that picker acts on it when a page has more than one.
Day content
A picker with days (or a date and time picker with slots) asks the
LiveView for the days it shows, a month or a few at a time, with the
koyomi:days event. Answer it with handle_days/4:
def handle_event("koyomi:days", params, socket) do
Koyomi.Live.handle_days(socket, params, &days/2)
end
# Called with the range of days and the id of the picker.
defp days(range, _id) do
for room <- MyApp.Rooms.availability(range), into: %{} do
{room.date, [label: "€#{room.price}", disabled: room.full?, marks: room.bookings]}
end
endhandle_days/4 returns the reply of the event ({:reply, map, socket}),
so it is the whole handle_event/3 clause. The callback returns a map,
or a list of {date, day} tuples, where each day is:
:disabled(orfalse) - the day may not be chosen;- a string - a short label under the day's number (a price, "Full");
- a keyword list or a map with:
:disabled-truewhen the day may not be chosen;:label- a short label under the day's number;:title- a longer description, read by screen readers and shown on hover ("3 rooms left");:tone-:accent,:success,:warning,:dangeror:muted, for the label and the day;:marks- dots under the number: a count, or a list of tones;:slots- the times that may be chosen on the day, for a date and time picker withslots: a list ofTime(wall clock times in the picker's zone), ofDateTime(shifted to the zone), or of maps with:time,:labeland:disabled.
Days that the callback leaves out have no content. A disabled day is
only refused in the browser: check it on the server too (a rule of
Koyomi.Rules, or your own validation).
With async: true, the callback runs in a task of the LiveView
(Phoenix.LiveView.start_async/3) and the reply says that the days will
come; give the result to handle_days_async/3:
def handle_event("koyomi:days", params, socket) do
Koyomi.Live.handle_days(socket, params, &days/2, async: true)
end
def handle_async({:koyomi_days, _id, _ref} = name, result, socket) do
{:noreply, Koyomi.Live.handle_days_async(socket, name, result)}
endWhen the days change on the server (a booking is made), call
refresh_days/2, and the picker asks again for the days it shows.
Summary
Functions
Closes the popover of a picker.
Moves the focus to a picker's field.
Answers a koyomi:days event: calls fun with the range of days that
the picker asks for and the picker's id, and replies with the days (see
the module docs).
Pushes the days of an async: true handle_days/4 to the picker.
name and result are the arguments of handle_async/3.
Turns the days that a callback returns into the map that the picker
reads, keyed by ISO date. time_zone is the picker's zone, for slots
given as DateTime.
Opens the popover of a picker.
Sets the value of a picker from the server, as if the person had picked
it: the picker shows it and posts it with the form (a phx-change
follows). value is a Date, a Date.Range, a list of Date, a
Time, a DateTime, a NaiveDateTime, a Koyomi.Recurrence, an ISO
string, or nil to clear it.
Asks a picker with days to load the days it shows again.
Functions
@spec close(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
Closes the popover of a picker.
@spec focus(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
Moves the focus to a picker's field.
@spec handle_days(Phoenix.LiveView.Socket.t(), map(), function(), keyword()) :: {:reply, map(), Phoenix.LiveView.Socket.t()}
Answers a koyomi:days event: calls fun with the range of days that
the picker asks for and the picker's id, and replies with the days (see
the module docs).
fun takes the range (Date.Range) and the id, or only the range.
params is the payload of the event (%{"id", "from", "to", "ref"});
a payload without valid days, or a span longer than 800 days,
replies with no days. A callback that raises replies with
%{"error" => true} and logs the error, so the picker shows that the
days could not be loaded.
Options:
:async- whentrue, runfunin a task (see the module docs).
@spec handle_days_async( Phoenix.LiveView.Socket.t(), {:koyomi_days, String.t(), term()}, term() ) :: Phoenix.LiveView.Socket.t()
Pushes the days of an async: true handle_days/4 to the picker.
name and result are the arguments of handle_async/3.
Turns the days that a callback returns into the map that the picker
reads, keyed by ISO date. time_zone is the picker's zone, for slots
given as DateTime.
Examples
iex> Koyomi.Live.normalize_days(%{~D[2027-03-15] => [label: "€90", marks: 2]})
%{"2027-03-15" => %{"label" => "€90", "marks" => ["accent", "accent"]}}
iex> Koyomi.Live.normalize_days([{~D[2027-03-16], :disabled}])
%{"2027-03-16" => %{"disabled" => true}}
@spec open(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
Opens the popover of a picker.
@spec push_value(Phoenix.LiveView.Socket.t(), String.t(), term()) :: Phoenix.LiveView.Socket.t()
Sets the value of a picker from the server, as if the person had picked
it: the picker shows it and posts it with the form (a phx-change
follows). value is a Date, a Date.Range, a list of Date, a
Time, a DateTime, a NaiveDateTime, a Koyomi.Recurrence, an ISO
string, or nil to clear it.
A picker also takes a new value (or field value) of a later render;
push_value/3 is for a value that the form does not have yet.
@spec refresh_days(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
Asks a picker with days to load the days it shows again.