View Source Rephex.AsyncAction (rephex v0.1.1)
Facilitates asynchronous operations in Phoenix LiveViews with enhanced state management.
Rephex.AsyncAction
seamlessly integrates with Phoenix.LiveView
to manage asynchronous tasks,
particularly useful for operations that require real-time feedback to the user,
such as loading data or performing long-running tasks.
- When executing
start/3
, ifPhoenix.LiveView.AsyncResult
is not in a loading state, it changes the specifiedAsyncResult
to a loading state before callingPhoenix.LiveView.start_async
. - Within the
start_async/4
function, calling theprogress
function allows for changing the loading state ofAsyncResult
. - If
start_async/4
returns a value,AsyncResult.ok
is called. In the case of an exception,AsyncResult.failed
is called.
Example:
# AsyncAction need Rephex state.
defmodule RephexPgWeb.State do
alias Phoenix.LiveView.AsyncResult
@initial_state %{
count: 0,
double_value: AsyncResult.ok(0),
add_twice_async: AsyncResult.ok(0)
}
use Rephex.State, initial_state: @initial_state
def add_count(socket, %{amount: amount} = _payload) when is_integer(amount) do
update_state_in(socket, [:count], &(&1 + amount))
end
end
# Minimal implementation
defmodule RephexPgWeb.State.HeavyDoubleAsync do
use Rephex.AsyncAction, result_path: [:double_value]
@impl true
def initial_progress(_path, _payload) do
# optional but recommended
# `start/4` apply this progress synchronously.
# AsyncResult.loading will be `{progress, _meta_values}` before start_async.
{0, 100}
end
@impl true
def start_async(_state, _path, %{amount: amount} = _payload, progress) do
# required
# This function will be passed to Phoenix's `start_async`.
max = 100
progress.({0, max})
1..max
|> Enum.each(fn i ->
:timer.sleep(2)
progress.({i, max})
end)
amount * 2
# AsyncAction will call `AsyncResult.ok(prev, amount)` on `handle_async`.
end
end
# Full implementation
defmodule RephexPgWeb.State.AddCountTwiceAsync do
alias Phoenix.LiveView
alias RephexPgWeb.State
@type payload :: %{amount: integer()}
@type progress :: {current :: non_neg_integer(), total :: non_neg_integer()}
@type cancel_reason :: any()
use Rephex.AsyncAction,
result_path: [:add_twice_async],
# You can pass types for functions implemented in macro.
payload_type: payload,
cancel_reason_type: cancel_reason,
progress_type: progress,
# You can suppress hyper frequent progress updates by setting throttle.
progress_throttle: 100
@impl true
def before_start(socket, _result_path, %{amount: _amount} = _payload) do
# optional
# This function will be called before `start_async`.
socket |> LiveView.put_flash(:info, "Add twice start")
end
@impl true
def initial_progress(_path, _payload) do
# optional
# This function will be called before `start_async` and determine the initial progress.
# AsyncResult.loading will be `{progress, _meta_values}` before start_async.
{0, 1}
end
@impl true
def start_async(_state, _path, %{amount: amount} = _payload, progress) do
# required
# This function will be passed to Phoenix's `start_async`.
max = 500
progress.({0, max})
1..max
|> Enum.each(fn i ->
:timer.sleep(2)
progress.({i, max})
end)
amount
end
@impl true
def after_resolve(socket, _result_path, result) do
# optional
# This function will be called after `start_async` is finished.
case result do
{:ok, amount} ->
socket
|> State.add_count(%{amount: amount})
|> LiveView.put_flash(:info, "Add twice done: #{amount}")
{:exit, _reason} ->
socket
|> LiveView.put_flash(:error, "Add twice failed")
end
end
@impl true
def generate_failed_value(_result_path, exit_reason) do
# optional
# You can customize the failed value.
case exit_reason do
{:shutdown, :cancel} -> "canceled by no-reason"
{:shutdown, {:cancel, text}} when is_bitstring(text) -> "canceled by #{text}"
_ -> "unknown reason"
end
end
end
# Usage in LiveView
defmodule RephexPgWeb.AccountLive.Index do
alias RephexPgWeb.State
use RephexPgWeb, :live_view
use Rephex.LiveView
alias Phoenix.LiveView.AsyncResult
@impl true
def mount(_params, _session, socket) do
{:ok, socket |> State.init()}
end
@impl true
def handle_event("start_heavy_double", %{"amount" => amount}, socket) do
{am, _} = Integer.parse(amount)
{:noreply, socket |> State.HeavyDoubleAsync.start(%{amount: am})}
end
@impl true
def handle_event("cancel_heavy_double", _params, socket) do
{:noreply, socket |> State.HeavyDoubleAsync.cancel()}
end
@impl true
def handle_event(
"start_add_count_twice",
%{"force" => force, "amount" => amount},
socket
)
when force in ["1", "0"] do
{am, _} = Integer.parse(amount)
{:noreply,
socket
|> State.AddCountTwiceAsync.start(%{amount: am}, restart_if_running: force == "1")}
end
@impl true
def handle_event("cancel_add_count_twice", _params, socket) do
{:noreply, socket |> State.AddCountTwiceAsync.cancel({:shutdown, {:cancel, "user cancel"}})}
end
@impl true
def handle_event(
"start_delayed_add",
%{"multi_key" => key, "amount" => amount},
socket
) do
{am, _} = Integer.parse(amount)
{:noreply, socket |> State.DelayedAddAsync.start(key, %{amount: am})}
end
def start_heavy_double_button(assigns) do
~H"""
<button class="border-2" phx-click="start_heavy_double" phx-value-amount={@amount}>
<%= @text %>
</button>
"""
end
def start_add_twice_button(assigns) do
~H"""
<button
class="border-2"
phx-click="start_add_count_twice"
phx-value-amount={@amount}
phx-value-force={@force}
>
<%= @text %>
</button>
"""
end
@impl true
def render(assigns) do
~H"""
<div class="border-2 m-5">
<div class="underline">Minimal AsyncAction Example</div>
<div>Double the amount with a delay. Async state is in AsyncResult.</div>
<%= case @rpx.double_value do %>
<% %AsyncResult{loading: {{current, max}, _meta}} -> %>
<%= "#{current} / #{max}" %>
<% %AsyncResult{failed: reason} when reason != nil -> %>
<div>Failed: <%= reason %></div>
<.start_heavy_double_button amount="2" force="0" text="Calculate double of 2" />
<% %AsyncResult{ok?: true, result: result} -> %>
Double of amount: <%= result %>
<.start_heavy_double_button amount="2" force="0" text="Calculate double of 2" />
<% end %>
</div>
<div>Count: <%= @rpx.count %></div>
<div class="border-2 m-5">
<div class="underline">Full-implemented AsyncAction Example</div>
<div>We can manipulate values not in AsyncResult.</div>
<%= case @rpx.add_twice_async do %>
<% %AsyncResult{loading: {{current, max}, _meta}} -> %>
<button class="border-2" phx-click="cancel_add_count_twice">
Cancel
</button>
<.start_add_twice_button amount="2" force="0" text="Start without option" />
<.start_add_twice_button amount="2" force="1" text="Force restart" />
<%= "#{current} / #{max}" %>
<% %AsyncResult{failed: reason} when reason != nil -> %>
<.start_add_twice_button amount="2" force="0" text="Async add 2" />
<div>Failed: <%= reason %></div>
<% _ -> %>
<.start_add_twice_button amount="2" force="0" text="Async add 2" />
<% end %>
</div>
"""
end
end