Packages

Rich, real-time user experiences with server-rendered HTML

Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
phoenix_live_view lib phoenix_live_view lifecycle.ex
Raw

lib/phoenix_live_view/lifecycle.ex

defmodule Phoenix.LiveView.Lifecycle do
@moduledoc false
alias Phoenix.LiveView.{Socket, Utils}
@lifecycle :lifecycle
@type hook :: map()
@type t :: %__MODULE__{
after_render: [hook],
handle_async: [hook],
handle_event: [hook],
handle_info: [hook],
handle_params: [hook],
mount: [hook]
}
defstruct after_render: [],
handle_async: [],
handle_event: [],
handle_info: [],
handle_params: [],
mount: []
@doc """
Returns a map of infos about the lifecycle stage for the given `view`.
"""
def stage_info(%Socket{} = socket, view, stage, arity) do
callbacks? = callbacks?(socket, stage)
exported? = function_exported?(view, stage, arity)
%{
any?: callbacks? or exported?,
callbacks?: callbacks?,
exported?: exported?
}
end
defp callbacks?(%Socket{private: %{@lifecycle => lifecycle}}, stage)
when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :mount] do
lifecycle |> Map.fetch!(stage) |> Kernel.!=([])
end
def attach_hook(%Socket{router: nil}, id, :handle_params, _fun) do
raise "cannot attach hook with id #{inspect(id)} on :handle_params because" <>
" the view was not mounted at the router with the live/3 macro"
end
def attach_hook(%Socket{} = socket, id, stage, fun)
when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :after_render] do
lifecycle = lifecycle(socket, stage)
hook = hook!(id, stage, fun)
existing = Enum.find(Map.fetch!(lifecycle, stage), &(&1.id == id))
if existing do
raise ArgumentError, """
existing hook #{inspect(hook.id)} already attached on #{inspect(hook.stage)}.
"""
end
update_lifecycle(socket, stage, fn hooks ->
hooks ++ [hook]
end)
end
def attach_hook(%Socket{}, _id, stage, _fun) do
raise ArgumentError, """
invalid lifecycle event provided to attach_hook.
Expected one of: :handle_async | :handle_event | :handle_info | :handle_params | :after_render
Got: #{inspect(stage)}
"""
end
def detach_hook(%Socket{} = socket, id, stage)
when stage in [:handle_async, :handle_event, :handle_info, :handle_params, :after_render] do
update_lifecycle(socket, stage, fn hooks ->
for hook <- hooks, hook.id != id, do: hook
end)
end
def detach_hook(%Socket{}, _id, stage) do
raise ArgumentError, """
invalid lifecycle event provided to detach_hook.
Expected one of: :handle_async | :handle_event | :handle_info | :handle_params | :after_render
Got: #{inspect(stage)}
"""
end
defp hook!(id, stage, fun) when is_atom(stage) and is_function(fun) do
%{id: id, stage: stage, function: fun}
end
defp lifecycle(socket, stage) do
if Utils.cid(socket) && stage not in [:after_render, :handle_event, :handle_async] do
raise ArgumentError, "lifecycle hooks are not supported on stateful components."
end
Map.fetch!(socket.private, @lifecycle)
end
defp update_lifecycle(socket, stage, fun) do
lifecycle = lifecycle(socket, stage)
new_lifecycle = Map.update!(lifecycle, stage, fun)
put_lifecycle(socket, new_lifecycle)
end
defp put_lifecycle(socket, lifecycle) do
put_private(socket, @lifecycle, lifecycle)
end
defp put_private(%Socket{private: private} = socket, key, value) when is_atom(key) do
%{socket | private: Map.put(private, key, value)}
end
@doc false
def validate_on_mount!(_view, {module, arg}) when is_atom(module) do
{module, arg}
end
def validate_on_mount!(_view, module) when is_atom(module) do
{module, :default}
end
def validate_on_mount!(view, result) do
raise ArgumentError, """
invalid on_mount hook declared in #{inspect(view)}.
Expected one of:
Module
{Module, arg}
Got: #{inspect(result)}
"""
end
@doc false
def prepare_on_mount!(hooks) do
for {module, _fun} = id <- hooks do
hook!(id, :mount, Function.capture(module, :on_mount, 4))
end
end
# Lifecycle Event API
@doc false
def build(mount_hooks) when is_list(mount_hooks) do
%__MODULE__{mount: prepare_on_mount!(mount_hooks)}
end
@doc false
def mount(params, session, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
reduce_socket(lifecycle.mount, socket, fn %{id: {mod, arg}} = hook, acc ->
case hook.function.(arg, params, session, acc) do
{:halt, %Socket{redirected: nil}} ->
raise_halt_without_redirect!(hook)
{:halt, %Socket{redirected: nil}, _opts} ->
raise_halt_without_redirect!(hook)
{:cont, %Socket{redirected: to}} when not is_nil(to) ->
raise_continue_with_redirect!(hook)
{:cont, %Socket{redirected: to}, _opts} when not is_nil(to) ->
raise_continue_with_redirect!(hook)
{:cont, socket, opts} ->
{:cont, Utils.handle_mount_options!(socket, opts, {mod, :on_mount, 4})}
ok ->
ok
end
end)
end
@doc false
def handle_event(event, val, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
reduce_handle_event(lifecycle.handle_event, socket, fn hook, acc ->
hook.function.(event, val, acc)
end)
end
defp reduce_handle_event([hook | hooks], acc, function) do
case function.(hook, acc) do
{:cont, %Socket{} = socket} -> reduce_handle_event(hooks, socket, function)
{:halt, %Socket{} = socket} -> {:halt, socket}
{:halt, reply, %Socket{} = socket} -> {:halt, reply, socket}
other -> bad_lifecycle_response!(other, hook)
end
end
defp reduce_handle_event([], acc, _function), do: {:cont, acc}
@doc false
def handle_params(params, uri, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
reduce_socket(lifecycle.handle_params, socket, fn hook, acc ->
hook.function.(params, uri, acc)
end)
end
@doc false
def handle_info(msg, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
reduce_socket(lifecycle.handle_info, socket, fn hook, acc ->
hook.function.(msg, acc)
end)
end
@doc false
def handle_async(key, result, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
reduce_socket(lifecycle.handle_async, socket, fn hook, acc ->
hook.function.(key, result, acc)
end)
end
@doc false
def after_render(%Socket{private: %{@lifecycle => lifecycle}} = socket) do
{:cont, new_socket} =
reduce_socket(lifecycle.after_render, socket, fn hook, acc ->
case hook.function.(acc) do
%Socket{} = new_socket ->
{:cont, new_socket}
other ->
raise ArgumentError,
"expected after_render hook to return a socket, got: #{inspect(other)}"
end
end)
new_socket
end
defp reduce_socket([hook | hooks], acc, function) do
case function.(hook, acc) do
{:cont, %Socket{} = socket} -> reduce_socket(hooks, socket, function)
{:halt, %Socket{} = socket} -> {:halt, socket}
other -> bad_lifecycle_response!(other, hook)
end
end
defp reduce_socket([], acc, _function), do: {:cont, acc}
defp bad_lifecycle_response!(result, hook) do
raise ArgumentError, """
invalid return from hook #{inspect(hook.id)} for lifecycle event #{inspect(hook.stage)}.
Expected one of:
#{expected_return(hook)}
Got: #{inspect(result)}
"""
end
defp expected_return(%{stage: :handle_event}) do
"""
{:cont, %Socket{}}
{:halt, %Socket{}}
{:halt, map, %Socket{}}
"""
end
defp expected_return(_) do
"""
{:cont, %Socket{}}
{:halt, %Socket{}}
"""
end
defp raise_halt_without_redirect!(hook) do
raise ArgumentError,
"the hook #{inspect(hook.id)} for lifecycle event :mount attempted to halt without redirecting."
end
defp raise_continue_with_redirect!(hook) do
raise ArgumentError,
"the hook #{inspect(hook.id)} for lifecycle event :mount attempted to redirect without halting."
end
end