defmodule Phoenix.LiveView do
@moduledoc ~S'''
A LiveView is a process that receives events, updates
its state, and renders updates to a page as diffs.
To get started, see [the Welcome guide](welcome.md).
This module provides advanced documentation and features
about using LiveView.
## Life-cycle
A LiveView begins as a regular HTTP request and HTML response,
and then upgrades to a stateful view on client connect,
guaranteeing a regular HTML page even if JavaScript is disabled.
Any time a stateful view changes or updates its socket assigns, it is
automatically re-rendered and the updates are pushed to the client.
Socket assigns are stateful values kept on the server side in
`Phoenix.LiveView.Socket`. This is different from the common stateless
HTTP pattern of sending the connection state to the client in the form
of a token or cookie and rebuilding the state on the server to service
every request.
You begin by rendering a LiveView typically from your router.
When LiveView is first rendered, the `c:mount/3` callback is invoked
with the current params, the current session and the LiveView socket.
As in a regular request, `params` contains public data that can be
modified by the user. The `session` always contains private data set
by the application itself. The `c:mount/3` callback wires up socket
assigns necessary for rendering the view. After mounting, `c:handle_params/3`
is invoked so uri and query params are handled. Finally, `c:render/1`
is invoked and the HTML is sent as a regular HTML response to the
client.
After rendering the static page, LiveView connects from the client
to the server where stateful views are spawned to push rendered updates
to the browser, and receive client events via `phx-` bindings. Just like
the first rendering, `c:mount/3`, is invoked with params, session,
and socket state. However in the connected client case, a LiveView process
is spawned on the server, runs `c:handle_params/3` again and then pushes
the result of `c:render/1` to the client and continues on for the duration
of the connection. If at any point during the stateful life-cycle a crash
is encountered, or the client connection drops, the client gracefully
reconnects to the server, calling `c:mount/3` and `c:handle_params/3` again.
LiveView also allows attaching hooks to specific life-cycle stages with
`attach_hook/4`.
## Template collocation
There are two possible ways of rendering content in a LiveView. The first
one is by explicitly defining a render function, which receives `assigns`
and returns a `HEEx` template defined with [the `~H` sigil](`Phoenix.Component.sigil_H/2`).
defmodule MyAppWeb.DemoLive do
use Phoenix.LiveView
def render(assigns) do
~H"""
Hello world!
"""
end
end
For larger templates, you can place them in a file in the same directory
and same name as the LiveView. For example, if the file above is placed
at `lib/my_app_web/live/demo_live.ex`, you can also remove the
`render/1` function altogether and put the template code at
`lib/my_app_web/live/demo_live.html.heex`.
## Async Operations
Performing asynchronous work is common in LiveViews and LiveComponents.
It allows the user to get a working UI quickly while the system fetches some
data in the background or talks to an external service, without blocking the
render or event handling. For async work, you also typically need to handle
the different states of the async operation, such as loading, error, and the
successful result. You also want to catch any errors or exits and translate it
to a meaningful update in the UI rather than crashing the user experience.
### Async assigns
The `assign_async/3` function takes the socket, a key or list of keys which will be assigned
asynchronously, and a function. This function will be wrapped in a `task` by
`assign_async`, making it easy for you to return the result. This function must
return an `{:ok, assigns}` or `{:error, reason}` tuple, where `assigns` is a map
of the keys passed to `assign_async`.
If the function returns anything else, an error is raised.
The task is only started when the socket is connected.
For example, let's say we want to async fetch a user's organization from the database,
as well as their profile and rank:
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
> ### Warning {: .warning}
>
> When using async operations it is important to not pass the socket into the function
> as it will copy the whole socket struct to the Task process, which can be very expensive.
>
> Instead of:
>
> ```elixir
> assign_async(:org, fn -> {:ok, %{org: fetch_org(socket.assigns.slug)}} end)
> ```
>
> We should do:
>
> ```elixir
> slug = socket.assigns.slug
> assign_async(:org, fn -> {:ok, %{org: fetch_org(slug)}} end)
> ```
>
> See: https://hexdocs.pm/elixir/process-anti-patterns.html#sending-unnecessary-data
The state of the async operation is stored in the socket assigns within an
`Phoenix.LiveView.AsyncResult`. It carries the loading and failed states, as
well as the result. For example, if we wanted to show the loading states in
the UI for the `:org`, our template could conditionally render the states:
```heex
<% end %>
If you prefer, you can also send a JavaScript script that immediately
reloads the page.
**Note:** only set `phx-track-static` on your own assets. For example, do
not set it in external JavaScript files:
Because you don't actually serve the file above, LiveView will interpret
the static above as missing, and this function will return true.
"""
def static_changed?(%Socket{private: private, endpoint: endpoint} = socket) do
if connect_params = private[:connect_params] do
connected?(socket) and
static_changed?(
connect_params["_track_static"],
endpoint.config(:cache_static_manifest_latest)
)
else
raise_root_and_mount_only!(socket, "static_changed?")
end
end
defp static_changed?([_ | _] = statics, %{} = latest) do
latest = Map.to_list(latest)
not Enum.all?(statics, fn static ->
[static | _] = :binary.split(static, "?")
Enum.any?(latest, fn {non_digested, digested} ->
String.ends_with?(static, non_digested) or String.ends_with?(static, digested)
end)
end)
end
defp static_changed?(_, _), do: false
defp raise_root_and_mount_only!(socket, fun) do
if child?(socket) do
raise RuntimeError, """
attempted to read #{fun} from a nested child LiveView #{inspect(socket.view)}.
Only the root LiveView has access to #{fun}.
"""
else
raise RuntimeError, """
attempted to read #{fun} outside of #{inspect(socket.view)}.mount/3.
#{fun} only exists while mounting. If you require access to this information
after mount, store the state in socket assigns.
"""
end
end
@doc ~S'''
Asynchronously updates a `Phoenix.LiveComponent` with new assigns.
The `pid` argument is optional and it defaults to the current process,
which means the update instruction will be sent to a component running
on the same LiveView. If the current process is not a LiveView or you
want to send updates to a live component running on another LiveView,
you should explicitly pass the LiveView's pid instead.
The second argument can be either the value of the `@myself` or the module of
the live component. If you pass the module, then the `:id` that identifies
the component must be passed as part of the assigns.
When the component receives the update,
[`update_many/1`](`c:Phoenix.LiveComponent.update_many/1`) will be invoked if
it is defined, otherwise [`update/2`](`c:Phoenix.LiveComponent.update/2`) is
invoked with the new assigns. If
[`update/2`](`c:Phoenix.LiveComponent.update/2`) is not defined all assigns
are simply merged into the socket. The assigns received as the first argument
of the [`update/2`](`c:Phoenix.LiveComponent.update/2`) callback will only
include the _new_ assigns passed from this function. Pre-existing assigns may
be found in `socket.assigns`.
While a component may always be updated from the parent by updating some
parent assigns which will re-render the child, thus invoking
[`update/2`](`c:Phoenix.LiveComponent.update/2`) on the child component,
`send_update/3` is useful for updating a component that entirely manages its
own state, as well as messaging between components mounted in the same
LiveView.
## Examples
def handle_event("cancel-order", _, socket) do
...
send_update(Cart, id: "cart", status: "cancelled")
{:noreply, socket}
end
def handle_event("cancel-order-asynchronously", _, socket) do
...
pid = self()
Task.Supervisor.start_child(MyTaskSup, fn ->
# Do something asynchronously
send_update(pid, Cart, id: "cart", status: "cancelled")
end)
{:noreply, socket}
end
def render(assigns) do
~H"""
<.some_component on_complete={&send_update(@myself, completed: &1)} />
"""
end
'''
def send_update(pid \\ self(), module_or_cid, assigns)
def send_update(pid, module, assigns) when is_atom(module) and is_pid(pid) do
assigns = Enum.into(assigns, %{})
id =
assigns[:id] ||
raise ArgumentError, "missing required :id in send_update. Got: #{inspect(assigns)}"
Phoenix.LiveView.Channel.send_update(pid, {module, id}, assigns)
end
def send_update(pid, %Phoenix.LiveComponent.CID{} = cid, assigns) when is_pid(pid) do
assigns = Enum.into(assigns, %{})
Phoenix.LiveView.Channel.send_update(pid, cid, assigns)
end
@doc """
Similar to `send_update/3` but the update will be delayed according to the given `time_in_milliseconds`.
It returns a reference which can be cancelled with `Process.cancel_timer/1`.
## Examples
def handle_event("cancel-order", _, socket) do
...
send_update_after(Cart, [id: "cart", status: "cancelled"], 3000)
{:noreply, socket}
end
def handle_event("cancel-order-asynchronously", _, socket) do
...
pid = self()
Task.start(fn ->
# Do something asynchronously
send_update_after(pid, Cart, [id: "cart", status: "cancelled"], 3000)
end)
{:noreply, socket}
end
"""
def send_update_after(pid \\ self(), module_or_cid, assigns, time_in_milliseconds)
def send_update_after(pid, %Phoenix.LiveComponent.CID{} = cid, assigns, time_in_milliseconds)
when is_integer(time_in_milliseconds) and is_pid(pid) do
assigns = Enum.into(assigns, %{})
Phoenix.LiveView.Channel.send_update_after(pid, cid, assigns, time_in_milliseconds)
end
def send_update_after(pid, module, assigns, time_in_milliseconds)
when is_atom(module) and is_integer(time_in_milliseconds) and is_pid(pid) do
assigns = Enum.into(assigns, %{})
id =
assigns[:id] ||
raise ArgumentError, "missing required :id in send_update_after. Got: #{inspect(assigns)}"
Phoenix.LiveView.Channel.send_update_after(pid, {module, id}, assigns, time_in_milliseconds)
end
@doc """
Returns the transport pid of the socket.
Raises `ArgumentError` if the socket is not connected.
## Examples
iex> transport_pid(socket)
#PID<0.107.0>
"""
def transport_pid(%Socket{}) do
case Process.get(:"$callers") do
[transport_pid | _] -> transport_pid
_ -> raise ArgumentError, "transport_pid/1 may only be called when the socket is connected."
end
end
defp child?(%Socket{parent_pid: pid}), do: is_pid(pid)
@doc """
Attaches the given `fun` by `name` for the lifecycle `stage` into `socket`.
> Note: This function is for server-side lifecycle callbacks.
> For client-side hooks, see the
> [JS Interop guide](js-interop.html#client-hooks-via-phx-hook).
Hooks provide a mechanism to tap into key stages of the LiveView
lifecycle in order to bind/update assigns, intercept events,
patches, and regular messages when necessary, and to inject
common functionality. Use `attach_hook/1` on any of the following
lifecycle stages: `:handle_params`, `:handle_event`, `:handle_info`, `:handle_async`, and
`:after_render`. To attach a hook to the `:mount` stage, use `on_mount/1`.
> Note: only `:after_render` and `:handle_event` hooks are currently supported in
> LiveComponents.
## Return Values
Lifecycle hooks take place immediately before a given lifecycle
callback is invoked on the LiveView. With the exception of `:after_render`,
a hook may return `{:halt, socket}` to halt the reduction, otherwise
it must return `{:cont, socket}` so the operation may continue until
all hooks have been invoked for the current stage.
For `:after_render` hooks, the `socket` itself must be returned.
Any updates to the socket assigns *will not* trigger a new render
or diff calculation to the client.
## Halting the lifecycle
Note that halting from a hook _will halt the entire lifecycle stage_.
This means that when a hook returns `{:halt, socket}` then the
LiveView callback will **not** be invoked. This has some
implications.
### Implications for plugin authors
When defining a plugin that matches on specific callbacks, you **must**
define a catch-all clause, as your hook will be invoked even for events
you may not be interested on.
### Implications for end-users
Allowing a hook to halt the invocation of the callback means that you can
attach hooks to intercept specific events before detaching themselves,
while allowing other events to continue normally.
## Replying to events
Hooks attached to the `:handle_event` stage are able to reply to client events
by returning `{:halt, reply, socket}`. This is useful especially for [JavaScript
interoperability](js-interop.html#client-hooks-via-phx-hook) because a client hook
can push an event and receive a reply.
## Examples
Attaching and detaching a hook:
def mount(_params, _session, socket) do
socket =
attach_hook(socket, :my_hook, :handle_event, fn
"very-special-event", _params, socket ->
# Handle the very special event and then detach the hook
{:halt, detach_hook(socket, :my_hook, :handle_event)}
_event, _params, socket ->
{:cont, socket}
end)
{:ok, socket}
end
Replying to a client event:
# JavaScript:
# /**
# * @type {Object.}
# */
# let Hooks = {}
# Hooks.ClientHook = {
# mounted() {
# this.pushEvent("ClientHook:mounted", {hello: "world"}, (reply) => {
# console.log("received reply:", reply)
# })
# }
# }
# let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
def render(assigns) do
~H"\""
"\""
end
def mount(_params, _session, socket) do
socket =
attach_hook(socket, :reply_on_client_hook_mounted, :handle_event, fn
"ClientHook:mounted", params, socket ->
{:halt, params, socket}
_, _, socket ->
{:cont, socket}
end)
{:ok, socket}
end
"""
defdelegate attach_hook(socket, name, stage, fun), to: Phoenix.LiveView.Lifecycle
@doc """
Detaches a hook with the given `name` from the lifecycle `stage`.
> Note: This function is for server-side lifecycle callbacks.
> For client-side hooks, see the
> [JS Interop guide](js-interop.html#client-hooks-via-phx-hook).
If no hook is found, this function is a no-op.
## Examples
def handle_event(_, socket) do
{:noreply, detach_hook(socket, :hook_that_was_attached, :handle_event)}
end
"""
defdelegate detach_hook(socket, name, stage), to: Phoenix.LiveView.Lifecycle
@doc ~S"""
Assigns a new stream to the socket or inserts items into an existing stream.
Returns an updated `socket`.
Streams are a mechanism for managing large collections on the client without
keeping the resources on the server.
* `name` - A string or atom name of the key to place under the
`@streams` assign.
* `items` - An enumerable of items to insert.
The following options are supported:
* `:at` - The index to insert or update the items in the
collection on the client. By default `-1` is used, which appends the items
to the parent DOM container. A value of `0` prepends the items.
Note that this operation is equal to inserting the items one by one, each at
the given index. Therefore, when inserting multiple items at an index other than `-1`,
the UI will display the items in reverse order:
stream(socket, :songs, [song1, song2, song3], at: 0)
In this case the UI will prepend `song1`, then `song2` and then `song3`, so it will show
`song3`, `song2`, `song1` and then any previously inserted items.
To insert in the order of the list, use `Enum.reverse/1`:
stream(socket, :songs, Enum.reverse([song1, song2, song3]), at: 0)
* `:reset` - A boolean to reset the stream on the client or not. Defaults
to `false`.
* `:limit` - An optional positive or negative number of results to limit
on the UI on the client. As new items are streamed, the UI will remove existing
items to maintain the limit. For example, to limit the stream to the last 10 items
in the UI while appending new items, pass a negative value:
stream(socket, :songs, songs, at: -1, limit: -10)
Likewise, to limit the stream to the first 10 items, while prepending new items,
pass a positive value:
stream(socket, :songs, songs, at: 0, limit: 10)
Once a stream is defined, a new `@streams` assign is available containing
the name of the defined streams. For example, in the above definition, the
stream may be referenced as `@streams.songs` in your template. Stream items
are temporary and freed from socket state immediately after the `render/1`
function is invoked (or a template is rendered from disk).
By default, calling `stream/4` on an existing stream will bulk insert the new items
on the client while leaving the existing items in place. Streams may also be reset
when calling `stream/4`, which we discuss below.
## Resetting a stream
To empty a stream container on the client, you can pass `:reset` with an empty list:
stream(socket, :songs, [], reset: true)
Or you can replace the entire stream on the client with a new collection:
stream(socket, :songs, new_songs, reset: true)
## Limiting a stream
It is often useful to limit the number of items in the UI while allowing the
server to stream new items in a fire-and-forget fashion. This prevents
the server from overwhelming the client with new results while also opening up
powerful features like virtualized infinite scrolling. See a complete
bidirectional infinite scrolling example with stream limits in the
[scroll events guide](bindings.md#scroll-events-and-infinite-stream-pagination)
When a stream exceeds the limit on the client, the existing items will be pruned
based on the number of items in the stream container and the limit direction. A
positive limit will prune items from the end of the container, while a negative
limit will prune items from the beginning of the container.
Note that the limit is not enforced on the first `mount/3` render (when no websocket
connection was established yet), as it means more data than necessary has been
loaded. In such cases, you should only load and pass the desired amount of items
to the stream.
When inserting single items using `stream_insert/4`, the limit needs to be passed
as an option for it to be enforced on the client:
stream_insert(socket, :songs, song, limit: -10)
## Required DOM attributes
For stream items to be trackable on the client, the following requirements
must be met:
1. The parent DOM container must include a `phx-update="stream"` attribute,
along with a unique DOM id.
2. Each stream item must include its DOM id on the item's element.
> #### Note {: .warning}
> Failing to place `phx-update="stream"` on the **immediate parent** for
> **each stream** will result in broken behavior.
>
> Also, do not alter the generated DOM ids, e.g., by prefixing them. Doing so will
> result in broken behavior.
When consuming a stream in a template, the DOM id and item is passed as a tuple,
allowing convenient inclusion of the DOM id for each item. For example:
```heex
<%= song.title %>
<%= song.duration %>
```
We consume the stream in a for comprehension by referencing the
`@streams.songs` assign. We used the computed DOM id to populate
the `
` id, then we render the table row as usual.
Now `stream_insert/3` and `stream_delete/3` may be issued and new rows will
be inserted or deleted from the client.
## Handling the empty case
When rendering a list of items, it is common to show a message for the empty case.
But when using streams, we cannot rely on `Enum.empty?/1` or similar approaches to
check if the list is empty. Instead we can use the CSS `:only-child` selector
and show the message client side:
```heex
No songs found
<%= song.title %>
<%= song.duration %>
```
## Non-stream items in stream containers
In the section on handling the empty case, we showed how to render a message when
the stream is empty by rendering a non-stream item inside the stream container.
Note that for non-stream items inside a `phx-update="stream"` container, the following
needs to be considered:
1. Items can be added and updated, but not removed, even if the stream is reset.
This means that if you try to conditionally render a non-stream item inside a stream container,
it won't be removed if it was rendered once.
2. Items are affected by the `:at` option.
For example, when you render a non-stream item at the beginning of the stream container and then
prepend items (with `at: 0`) to the stream, the non-stream item will be pushed down.
"""
@spec stream(%Socket{}, name :: atom | String.t(), items :: Enumerable.t(), opts :: Keyword.t()) ::
%Socket{}
def stream(%Socket{} = socket, name, items, opts \\ []) do
socket
|> ensure_streams()
|> assign_stream(name, items, opts)
end
@doc ~S"""
Configures a stream.
The following options are supported:
* `:dom_id` - An optional function to generate each stream item's DOM id.
The function accepts each stream item and converts the item to a string id.
By default, the `:id` field of a map or struct will be used if the item has
such a field, and will be prefixed by the `name` hyphenated with the id.
For example, the following examples are equivalent:
stream(socket, :songs, songs)
socket
|> stream_configure(:songs, dom_id: &("songs-#{&1.id}"))
|> stream(:songs, songs)
A stream must be configured before items are inserted, and once configured,
a stream may not be re-configured. To ensure a stream is only configured a
single time in a LiveComponent, use the `mount/1` callback. For example:
def mount(socket) do
{:ok, stream_configure(socket, :songs, dom_id: &("songs-#{&1.id}"))}
end
def update(assigns, socket) do
{:ok, stream(socket, :songs, ...)}
end
Returns an updated `socket`.
"""
@spec stream_configure(%Socket{}, name :: atom | String.t(), opts :: Keyword.t()) :: %Socket{}
def stream_configure(%Socket{} = socket, name, opts) when is_list(opts) do
new_socket = ensure_streams(socket)
case new_socket.assigns.streams do
%{^name => %LiveStream{}} ->
raise ArgumentError, "cannot configure stream :#{name} after it has been streamed"
%{__configured__: %{^name => _opts}} ->
raise ArgumentError, "cannot re-configure stream :#{name} after it has been configured"
%{} ->
Phoenix.Component.update(new_socket, :streams, fn streams ->
Map.update!(streams, :__configured__, fn conf -> Map.put(conf, name, opts) end)
end)
end
end
defp ensure_streams(%Socket{} = socket) do
Phoenix.LiveView.Utils.assign_new(socket, :streams, fn ->
%{__ref__: 0, __changed__: MapSet.new(), __configured__: %{}}
end)
end
@doc """
Inserts a new item or updates an existing item in the stream.
Returns an updated `socket`.
See `stream/4` for inserting multiple items at once.
The following options are supported:
* `:at` - The index to insert or update the item in the collection on the client.
By default, the item is appended to the parent DOM container. This is the same as
passing a limit of `-1`.
If the item already exists in the parent DOM container then it will be
updated in place.
* `:limit` - A limit of items to maintain in the UI. A limit passed to `stream/4` does
not affect subsequent calls to `stream_insert/4`, therefore the limit must be passed
here as well in order to be enforced. See `stream/4` for more information on
limiting streams.
## Examples
Imagine you define a stream on mount with a single item:
stream(socket, :songs, [%Song{id: 1, title: "Song 1"}])
Then, in a callback such as `handle_info` or `handle_event`, you
can append a new song:
stream_insert(socket, :songs, %Song{id: 2, title: "Song 2"})
Or prepend a new song with `at: 0`:
stream_insert(socket, :songs, %Song{id: 2, title: "Song 2"}, at: 0)
Or update an existing song (in this case the `:at` option has no effect):
stream_insert(socket, :songs, %Song{id: 1, title: "Song 1 updated"}, at: 0)
Or append a new song while limiting the stream to the last 10 items:
stream_insert(socket, :songs, %Song{id: 2, title: "Song 2"}, limit: -10)
## Updating Items
As shown, an existing item on the client can be updated by issuing a `stream_insert`
for the existing item. When the client updates an existing item, the item will remain
in the same location as it was previously, and will not be moved to the end of the
parent children. To both update an existing item and move it to another position,
issue a `stream_delete`, followed by a `stream_insert`. For example:
song = get_song!(id)
socket
|> stream_delete(:songs, song)
|> stream_insert(:songs, song, at: -1)
See `stream_delete/3` for more information on deleting items.
"""
@spec stream_insert(%Socket{}, name :: atom | String.t(), item :: any, opts :: Keyword.t()) ::
%Socket{}
def stream_insert(%Socket{} = socket, name, item, opts \\ []) do
at = Keyword.get(opts, :at, -1)
limit = Keyword.get(opts, :limit)
update_stream(socket, name, &LiveStream.insert_item(&1, item, at, limit))
end
@doc """
Deletes an item from the stream.
The item's DOM is computed from the `:dom_id` provided in the `stream/3` definition.
Delete information for this DOM id is sent to the client and the item's element
is removed from the DOM, following the same behavior of element removal, such as
invoking `phx-remove` commands and executing client hook `destroyed()` callbacks.
## Examples
def handle_event("delete", %{"id" => id}, socket) do
song = get_song!(id)
{:noreply, stream_delete(socket, :songs, song)}
end
See `stream_delete_by_dom_id/3` to remove an item without requiring the
original data structure.
Returns an updated `socket`.
"""
@spec stream_delete(%Socket{}, name :: atom | String.t(), item :: any) :: %Socket{}
def stream_delete(%Socket{} = socket, name, item) do
update_stream(socket, name, &LiveStream.delete_item(&1, item))
end
@doc ~S'''
Deletes an item from the stream given its computed DOM id.
Returns an updated `socket`.
Behaves just like `stream_delete/3`, but accept the precomputed DOM id,
which allows deleting from a stream without fetching or building the original
stream data structure.
## Examples
def render(assigns) do
~H"""
<%= song.title %>
"""
end
def handle_event("delete", %{"id" => dom_id}, socket) do
{:noreply, stream_delete_by_dom_id(socket, :songs, dom_id)}
end
'''
@spec stream_delete_by_dom_id(%Socket{}, name :: atom | String.t(), id :: String.t()) ::
%Socket{}
def stream_delete_by_dom_id(%Socket{} = socket, name, id) do
update_stream(socket, name, &LiveStream.delete_item_by_dom_id(&1, id))
end
defp assign_stream(%Socket{} = socket, name, items, opts) do
streams = socket.assigns.streams
case streams do
%{^name => %LiveStream{}} ->
new_socket =
if opts[:reset] do
update_stream(socket, name, &LiveStream.reset(&1))
else
socket
end
Enum.reduce(items, new_socket, fn item, acc -> stream_insert(acc, name, item, opts) end)
%{} ->
config = get_in(streams, [:__configured__, name]) || []
opts = Keyword.merge(opts, config)
ref =
if cid = socket.assigns[:myself] do
"#{cid}-#{streams.__ref__}"
else
to_string(streams.__ref__)
end
stream = LiveStream.new(name, ref, items, opts)
socket
|> Phoenix.Component.update(:streams, fn streams ->
%{streams | __ref__: streams.__ref__ + 1}
|> Map.put(name, stream)
|> Map.update!(:__changed__, &MapSet.put(&1, name))
end)
|> attach_hook(name, :after_render, fn hook_socket ->
if name in hook_socket.assigns.streams.__changed__ do
Phoenix.Component.update(hook_socket, :streams, fn streams ->
streams
|> Map.update!(:__changed__, &MapSet.delete(&1, name))
|> Map.update!(name, &LiveStream.prune(&1))
end)
else
hook_socket
end
end)
end
end
defp update_stream(%Socket{} = socket, name, func) do
Phoenix.Component.update(socket, :streams, fn streams ->
stream =
case Map.fetch(streams, name) do
{:ok, stream} -> stream
:error -> raise ArgumentError, "no stream with name #{inspect(name)} previously defined"
end
streams
|> Map.put(name, func.(stream))
|> Map.update!(:__changed__, &MapSet.put(&1, name))
end)
end
@doc """
Assigns keys asynchronously.
Wraps your function in a task linked to the caller, errors are wrapped.
Each key passed to `assign_async/3` will be assigned to
an `%AsyncResult{}` struct holding the status of the operation
and the result when the function completes.
The task is only started when the socket is connected.
## Options
* `:supervisor` - allows you to specify a `Task.Supervisor` to supervise the task.
* `:reset` - remove previous results during async operation when true. Possible values are
`true`, `false`, or a list of keys to reset. Defaults to `false`.
## Examples
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
See the moduledoc for more information.
## `assign_async/3` and `send_update/3`
Since the code inside `assign_async/3` runs in a separate process,
`send_update(Component, data)` does not work inside `assign_async/3`,
since `send_update/2` assumes it is running inside the LiveView process.
The solution is to explicitly send the update to the LiveView:
parent = self()
assign_async(socket, :org, fn ->
# ...
send_update(parent, Component, data)
end)
"""
defmacro assign_async(socket, key_or_keys, func, opts \\ []) do
Async.assign_async(socket, key_or_keys, func, opts, __CALLER__)
end
@doc """
Wraps your function in an asynchronous task and invokes a callback `name` to
handle the result.
The task is linked to the caller and errors/exits are wrapped.
The result of the task is sent to the `c:handle_async/3` callback
of the caller LiveView or LiveComponent.
The task is only started when the socket is connected.
## Options
* `:supervisor` - allows you to specify a `Task.Supervisor` to supervise the task.
## Examples
def mount(%{"id" => id}, _, socket) do
{:ok,
socket
|> assign(:org, AsyncResult.loading())
|> start_async(:my_task, fn -> fetch_org!(id) end)}
end
def handle_async(:my_task, {:ok, fetched_org}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end
def handle_async(:my_task, {:exit, reason}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end
See the moduledoc for more information.
"""
defmacro start_async(socket, name, func, opts \\ []) do
Async.start_async(socket, name, func, opts, __CALLER__)
end
@doc """
Cancels an async operation if one exists.
Accepts either the `%AsyncResult{}` when using `assign_async/3` or
the key passed to `start_async/3`.
The underlying process will be killed with the provided reason, or
with `{:shutdown, :cancel}` if no reason is passed. For `assign_async/3`
operations, the `:failed` field will be set to `{:exit, reason}`.
For `start_async/3`, the `c:handle_async/3` callback will receive
`{:exit, reason}` as the result.
Returns the `%Phoenix.LiveView.Socket{}`.
## Examples
cancel_async(socket, :preview)
cancel_async(socket, :preview, :my_reason)
cancel_async(socket, socket.assigns.preview)
"""
def cancel_async(socket, async_or_keys, reason \\ {:shutdown, :cancel}) do
Async.cancel_async(socket, async_or_keys, reason)
end
end