# `Koyomi.Live`
[🔗](https://github.com/hattoriai/koyomi/blob/v0.2.1/lib/koyomi/live.ex#L1)

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.

# `close`

```elixir
@spec close(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
```

Closes the popover of a picker.

# `focus`

```elixir
@spec focus(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
```

Moves the focus to a picker's field.

# `handle_days`

```elixir
@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`

```elixir
@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`

```elixir
@spec normalize_days(map() | list(), String.t() | nil) :: %{
  required(String.t()) =&gt; 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`

```elixir
@spec open(Phoenix.LiveView.Socket.t(), String.t()) :: Phoenix.LiveView.Socket.t()
```

Opens the popover of a picker.

# `push_value`

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

# `refresh_days`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
