Koyomi.Live (Koyomi v0.2.1)

Copy Markdown View Source

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
end

handle_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 (or false) - 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 - true when 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, :danger or :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 with slots: a list of Time (wall clock times in the picker's zone), of DateTime (shifted to the zone), or of maps with :time, :label and :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)}
end

When 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

close(socket, id)

Closes the popover of a picker.

focus(socket, id)

Moves the focus to a picker's field.

handle_days(socket, params, fun, opts \\ [])

@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 - when true, run fun in a task (see the module docs).

handle_days_async(socket, arg, result)

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

normalize_days(days, time_zone \\ nil)

@spec normalize_days(map() | list(), String.t() | nil) :: %{
  required(String.t()) => map()
}

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

open(socket, id)

Opens the popover of a picker.

push_value(socket, id, value)

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.

refresh_days(socket, id)

Asks a picker with days to load the days it shows again.