# `AshPhoenix.LiveView`
[🔗](https://github.com/ash-project/ash_phoenix/blob/v2.3.21/lib/ash_phoenix/live_view.ex#L5)

Utilities for keeping Ash query results up to date in a LiveView.

# `assign`

```elixir
@type assign() :: atom()
```

# `assigns`

```elixir
@type assigns() :: map()
```

# `callback`

```elixir
@type callback() ::
  (socket() -&gt; callback_result())
  | (socket(), Keyword.t() | nil -&gt; callback_result())
```

# `callback_result`

```elixir
@type callback_result() :: struct() | [struct()] | Ash.Page.page() | nil
```

# `liveness_options`

```elixir
@type liveness_options() :: Keyword.t()
```

# `page_params`

```elixir
@type page_params() :: %{required(String.t()) =&gt; String.t()}
```

# `socket`

```elixir
@type socket() :: term()
```

# `topic`

```elixir
@type topic() :: String.t()
```

# `assign_page_and_stream_result`

```elixir
@spec assign_page_and_stream_result(
  Phoenix.LiveView.Socket.t(),
  Ash.Page.page(),
  Keyword.t()
) :: Phoenix.LiveView.Socket.t()
```

Shorthand to add results of a page onto a socket along with the page.

### Examples:

```elixir
AshPhoenix.LiveView.assign_page_and_stream_result(%Phoenix.LiveView.Socket{}, %Ash.Page.Offset{results: [1,2,3]})
# => %Phoenix.LiveView.Socket{assigns: %{streams: %{results: [1,2,3]}, page: %Ash.Page.Offset{results: nil}}}
```

## Options

* `:results_key` (`t:atom/0`) -  The default value is `:results`.

* `:page_key` (`t:atom/0`) -  The default value is `:page`.

* `:stream_opts` (`t:keyword/0`) -  The default value is `[reset: true]`.

# `can_link_to_page?`

# `change_page`

# `handle_live`

Incorporates an `Ash.Notifier.Notification` into the query results, based on the liveness configuration.

You will want to match on receiving a notification from Ash, and the easiest way to do that is to match
on the payload like so:

```
@impl true
def handle_info(%{topic: topic, payload: %Ash.Notifier.Notification{}}, socket) do
  {:noreply, handle_live(socket, topic, [:query1, :query2, :query3])}
end
```

Feel free to intercept notifications and do your own logic to respond to events. Ultimately, all
that matters is that you also call `handle_live/3` if you want it to update your query results.

The assign or list of assigns passed as the third argument must be the same names passed into
`keep_live`. If you only want some queries to update based on some events, you can define multiple
matches on events, and only call `handle_live/3` with the assigns that should be updated for that
notification.

# `keep_live`

```elixir
@spec keep_live(socket(), assign(), callback(), liveness_options()) :: socket()
```

Runs the callback, and stores the information required to keep it live in the socket assigns.

The data will be assigned to the provided key, e.g `keep_live(socket, :me, ...)` would assign the results
to `:me` (accessed as `@me` in the template).

Additionally, you'll need to define a `handle_info/2` callback for your liveview to receive any
notifications, and pass that notification into `handle_live/3`. See `handle_live/3` for more.

## Important

The logic for handling events to keep data live is currently very limited. It will simply rerun the query
every time. To this end, you should feel free to intercept individual events and handle them yourself for
more optimized liveness.

## Pagination

To make paginated views convenient, as well as making it possible to keep those views live, Ash does not
simply rerun the query when it gets an update, as that could involve shuffling the records around on the
page. Eventually this will be configurable, but for now, Ash simply adjusts the query to only include the
records that are on the page. If a record would be removed from a page due to a data change, it will simply
be left there. For the best performance, use `keyset` pagination. If you *need* the ability to jump to a
page by number, you'll want to use `offset` pagination, but keep in mind that it performs worse on large
tables.

To support this, accept a second parameter to your callback function, which will be the options to use in `page_opts`

## Options:
* `:subscribe` - A topic or list of topics that should cause this data to update. Can also be a 1-arity function that receives the fetch result and returns a topic or list of topics, allowing dynamic subscriptions based on the fetched data.

* `:pub_sub` (`t:atom/0`) - A module with `subscribe/1` and `unsubscribe/1` functions (e.g. a Phoenix endpoint) to use instead of `socket.endpoint`.

* `:refetch?` (`t:boolean/0`) - A boolean flag indicating whether a refetch is allowed to happen. Defaults to `true`

* `:after_fetch` (`t:term/0`) - A two argument function that takes the results, and the socket, and returns the new socket. Can be used to set assigns based on the result of the query.

* `:results` - For list and page queries, by default the records shown are never changed (unless the page changes) Valid values are :keep, :lose The default value is `:keep`.

* `:load_until_connected?` (`t:boolean/0`) - If the socket is not connected, then the value of the provided assign is set to `:loading`. Has no effect if `initial` is provided.

* `:initial` (`t:term/0`) - Results to use instead of running the query immediately.

* `:refetch_interval` (`t:non_neg_integer/0`) - An interval (in ms) to periodically refetch the query

* `:refetch_window` (`t:non_neg_integer/0`) - The minimum time (in ms) between refetches, including refetches caused by notifications.

A great way to get readable millisecond values is to use the functions in erlang's `:timer` module,
like `:timer.hours/1`, `:timer.minutes/1`, and `:timer.seconds/1`

#### refetch_interval

If this option is set, a message is sent as `{:refetch, assign_name, opts}` on that interval.
You can then match on that event, like so:

```
def handle_info({:refetch, assign, opts}, socket) do
  {:noreply, handle_live(socket, :refetch, assign, opts)}
end
```

This is the equivalent of `:timer.send_interval(interval, {:refetch, assign, opts})`, so feel free to
roll your own solution if you have complex refetching requirements.

#### refetch_window

Normally, when a pubsub message is received the query is rerun. This option will cause the query to wait at least
this amount of time before doing a refetch. This is accomplished with `Process.send_after/4`, and recording the
last time each query was refetched. For example if a refetch happens at time `0`, and the `refetch_window` is
10,000 ms, we would refetch, and record the time. Then if another refetch should happen 5,000 ms later, we would
look and see that we need to wait another 5,000ms. So we use `Process.send_after/4` to send a
`{:refetch, assign, opts}` message in 5,000ms. The time that a refetch was requested is tracked, so if the
data has since been refetched, it won't be refetched again.

#### Future Plans

One interesting thing here is that, given that we know the scope of data that a resource cares about,
we should be able to make optimizations to this code, to support partial refetches, or even just updating
the data directly. However, this will need to be carefully considered, as the risks involve showing users
data they could be unauthorized to see, or having state in the socket that is inconsistent.

# `last_page`

# `next_page?`

```elixir
@spec next_page?(Ash.Page.page()) :: boolean()
```

Returns true if there's a next page.

### Examples

    iex> AshPhoenix.LiveView.next_page?(%Ash.Page.Offset{offset: 10, limit: 10, more?: true})
    true

    iex> AshPhoenix.LiveView.next_page?(%{offset: 0, limit: 10, more?: false})
    false

# `on_page?`

# `page_from_params`

> This function is deprecated. Use params_to_page_opts/2 instead.

```elixir
@spec page_from_params(page_params(), pos_integer(), boolean()) :: Keyword.t()
```

Generates a page request for doing pagination based on the passed in parameters.

## Examples

    iex> AshPhoenix.LiveView.page_from_params(%{"offset" => "10", "limit" => "10"}, 20, true)
    [count: true, limit: 10, offset: 10]

    iex> AshPhoenix.LiveView.page_from_params(%{"offset" => "10", "limit" => "10"}, 20)
    [count: false, limit: 10, offset: 10]

    iex> AshPhoenix.LiveView.page_from_params(%{"offset" => "10", "count" => "true"}, 20)
    [count: true, limit: 20, offset: 10]

# `page_link_params`

```elixir
@spec page_link_params(Ash.Page.Offset.t(), String.t() | pos_integer()) ::
  [any()] | :invalid
```

Converts Ash.Page.Offset to query link params

Options:

* "first" - first page
* "prev"  - prev page
* "next"  - next page
* "last"  - last page (if the page is loaded with `count: true`)

Returns `:invalid` or a list of query link params.

# `page_number`

# `page_params`

```elixir
@spec page_params(Ash.Page.page()) :: Keyword.t()
```

Converts an `Ash.Page.Keyset` or `Ash.Page.Offset` struct into page parameters in keyword format.

# `params_to_page_opts`

```elixir
@spec params_to_page_opts(page_params(), Keyword.t()) :: Keyword.t()
```

Generates page request options for pagination based on the passed in parameters and options.

## Options

* `:default_limit` (`t:pos_integer/0`) - The default limit to use if not provided in params. The default value is `250`.

* `:count?` (`t:boolean/0`) - Whether to include a count query. The default value is `false`.

## Counting records

Count queries are **only** included if `count?: true` is explicitly set in options.

Unlike `page_from_params/3`, this function does **not** override `:count` based on user params, even if params map contains "count" key.
This makes developer intent more explicit, and prevents potentially expensive queries from being run unexpectedly.

If you'd like to include a count query from user params, you can explicitly do:

```elixir
AshPhoenix.LiveView.params_to_page_opts(params, count?: params["count"] == "true")
```

## Examples

    iex> AshPhoenix.LiveView.params_to_page_opts(%{"offset" => "10", "limit" => "10"}, default_limit: 20, count?: true)
    [count: true, limit: 10, offset: 10]

    iex> AshPhoenix.LiveView.params_to_page_opts(%{"offset" => "10", "limit" => "10"})
    [count: false, limit: 10, offset: 10]

    iex> AshPhoenix.LiveView.params_to_page_opts(%{"offset" => "10", "count" => "true"}, default_limit: 20)
    [count: false, limit: 20, offset: 10]

### User params

This function safely ignores any keys in the params map that are not related to pagination (`"after"`, `"before"`, `"limit"`, `"offset"`).
You can pass the entire user params map directly to this function since extra keys will be ignored.

This makes it easy to use with LiveView's `handle_params/3` callback, for example:

```elixir
def handle_params(params, _url, socket) do
  page_opts = AshPhoenix.LiveView.params_to_page_opts(params, default_limit: @limit)

  page = MyDomain.search_resources!(query_text, page: page_opts)

  {:noreply, assign(socket, :page, page)}
end
```

This allows you to change the pagination type (keyset or offset) without changing your view code.

# `prev_page?`

```elixir
@spec prev_page?(Ash.Page.page()) :: boolean()
```

Returns true if there's a previous page.

### Examples

    iex> AshPhoenix.LiveView.prev_page?(%Ash.Page.Offset{offset: 10, limit: 10})
    true

    iex> AshPhoenix.LiveView.prev_page?(%{offset: 0, limit: 10})
    false

---

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