Packages
phoenix_live_view
1.1.7
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.3
1.2.0-rc.2
1.2.0-rc.1
1.2.0-rc.0
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.23
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc.4
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
retired
1.0.7
1.0.6
retired
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.14
0.17.13
0.17.12
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.6.0-dev
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
Rich, real-time user experiences with server-rendered HTML
Current section
Files
Jump to
Current section
Files
lib/phoenix_live_view/static.ex
defmodule Phoenix.LiveView.ReloadError do
defexception [:message, :plug_status]
end
defmodule Phoenix.LiveView.Static do
# Holds the logic for static rendering.
@moduledoc false
alias Phoenix.LiveView.{Socket, Utils, Diff, Route, Lifecycle}
# Token version. Should be changed whenever new data is stored.
@token_vsn 6
@phoenix_reload_status "__phoenix_reload_status__"
def token_vsn, do: @token_vsn
# Max session age in seconds. Equivalent to 2 weeks.
@max_session_age 1_209_600
@doc """
Acts as a view via put_view to maintain the
controller render + instrumentation stack.
"""
def render(_, %{content: content}) do
content
end
@doc """
Verifies a LiveView token.
"""
def verify_token(endpoint, token) do
case Phoenix.Token.verify(endpoint, Utils.salt!(endpoint), token, max_age: @max_session_age) do
{:ok, {@token_vsn, term}} -> {:ok, term}
{:ok, _} -> {:error, :outdated}
{:error, :missing} -> {:error, :invalid}
{:error, reason} when reason in [:expired, :invalid] -> {:error, reason}
end
end
defp live_session(%Plug.Conn{} = conn) do
case conn.private[:phoenix_live_view] do
{_view, _opts, %{name: _name, extra: _extra} = lv_session} -> lv_session
nil -> nil
end
end
defp load_session(conn_or_socket_session, opts) do
user_session = Keyword.get(opts, :session, %{})
validate_session(user_session)
{user_session, Map.merge(conn_or_socket_session, user_session)}
end
defp validate_session(session) do
if is_map(session) and Enum.all?(session, fn {k, _} -> is_binary(k) end) do
:ok
else
raise ArgumentError,
"LiveView :session must be a map with string keys, got: #{inspect(session)}"
end
end
defp maybe_get_session(conn) do
Plug.Conn.get_session(conn)
rescue
_ -> %{}
end
defp maybe_put_live_layout(private, %{extra: %{layout: layout}}) do
Map.put(private, :live_layout, layout)
end
defp maybe_put_live_layout(private, _live_session) do
private
end
@doc """
Renders a live view without spawning a LiveView server.
* `conn` - the Plug.Conn struct form the HTTP request
* `view` - the LiveView module
## Options
* `:router` - the router the live view was built at
* `:action` - the router action
* `:session` - the required map of session data
* `:container` - the optional tuple for the HTML tag and DOM attributes
"""
def render(%Plug.Conn{} = conn, view, opts) do
endpoint = Phoenix.Controller.endpoint_module(conn)
case conn.req_cookies do
%Plug.Conn.Unfetched{} ->
do_render(conn, endpoint, view, opts)
%{@phoenix_reload_status => status_token} ->
conn = Plug.Conn.delete_resp_cookie(conn, @phoenix_reload_status)
{status, exception, errored_view, stack} =
case verify_token(endpoint, status_token) do
{:ok, %{status: status, exception: exception, view: errored_view, stack: stack}}
when is_integer(status) ->
{status, exception, errored_view, stack}
{:error, _reason} ->
{500, nil, nil, []}
end
message = """
#{errored_view} raised #{exception} during connected mount sending a #{status} response
"""
raise Plug.Conn.WrapperError,
conn: conn,
kind: :error,
reason: %Phoenix.LiveView.ReloadError{message: message, plug_status: status},
stack: stack
%{} ->
do_render(conn, endpoint, view, opts)
end
end
defp do_render(%Plug.Conn{} = conn, endpoint, view, opts) do
conn_session = maybe_get_session(conn)
{to_sign_session, mount_session} = load_session(conn_session, opts)
live_session = live_session(conn)
config = load_live!(view, :view)
lifecycle = lifecycle(config, live_session)
{tag, extended_attrs} = container(config, opts)
router = Keyword.get(opts, :router)
action = Keyword.get(opts, :action)
flash = Map.get(conn.assigns, :flash) || Map.get(conn.private, :phoenix_flash, %{})
request_url = Plug.Conn.request_url(conn)
host_uri = URI.parse(request_url)
socket =
Utils.configure_socket(
%Socket{endpoint: endpoint, view: view, router: router},
%{
assign_new: {conn.assigns, []},
connect_params: %{},
connect_info: conn,
conn_session: conn_session,
lifecycle: lifecycle,
root_view: view,
live_temp: %{}
}
|> maybe_put_live_layout(live_session),
action,
flash,
host_uri
)
case call_mount_and_handle_params!(socket, view, mount_session, conn.params, request_url) do
{:ok, socket} ->
data_attrs = [
phx_session: sign_root_session(socket, router, view, to_sign_session, live_session),
phx_static: sign_static_token(socket)
]
data_attrs = if(router, do: [phx_main: true], else: []) ++ data_attrs
attrs = [
{:id, socket.id},
{:data, data_attrs}
| extended_attrs
]
try do
{:ok, to_rendered_content_tag(socket, tag, view, attrs), socket.assigns}
catch
:throw, {:phoenix, :child_redirect, redirected, flash} ->
{:stop, Utils.replace_flash(%{socket | redirected: redirected}, flash)}
end
{:stop, socket} ->
{:stop, socket}
end
end
@doc """
Renders a nested live view without spawning a server.
* `parent` - the parent `%Phoenix.LiveView.Socket{}`
* `view` - the child LiveView module
Accepts the same options as `render/3`.
"""
def nested_render(
%Socket{endpoint: endpoint, transport_pid: transport_pid} = parent,
view,
opts
) do
config = load_live!(view, :view)
container = container(config, opts)
sticky? = Keyword.get(opts, :sticky, false)
child_id =
opts[:id] ||
raise ArgumentError,
"an :id is required when rendering child LiveView. " <>
"The :id must uniquely identify the child."
socket =
Utils.configure_socket(
%Socket{
id: to_string(child_id),
view: view,
endpoint: endpoint,
root_pid: if(sticky?, do: nil, else: parent.root_pid),
parent_pid: if(sticky?, do: nil, else: self()),
sticky?: sticky?,
router: parent.router
},
%{
assign_new: {parent.assigns.__assigns__, []},
lifecycle: config.lifecycle,
live_layout: false,
root_view: if(sticky?, do: view, else: parent.private.root_view),
live_temp: %{}
},
nil,
%{},
parent.host_uri
)
if transport_pid do
connected_nested_render(parent, socket, view, container, opts, sticky?)
else
disconnected_nested_render(parent, socket, view, container, opts, sticky?)
end
end
defp disconnected_nested_render(parent, socket, view, container, opts, sticky?) do
conn_session = parent.private.conn_session
{to_sign_session, mount_session} = load_session(conn_session, opts)
{tag, extended_attrs} = container
socket = put_in(socket.private[:conn_session], conn_session)
socket =
Utils.maybe_call_live_view_mount!(socket, view, :not_mounted_at_router, mount_session)
session_token =
if sticky?, do: sign_nested_session(parent, socket, view, to_sign_session, sticky?)
if redir = socket.redirected do
throw({:phoenix, :child_redirect, redir, Utils.get_flash(socket)})
end
if Lifecycle.stage_info(socket, view, :handle_params, 3).any? do
raise ArgumentError, "handle_params/3 is not allowed on child LiveViews, only at the root"
end
attrs = [
{:id, socket.id},
{:data,
[
phx_session: session_token || "",
phx_static: sign_static_token(socket)
] ++ if(sticky?, do: [phx_sticky: true], else: [phx_parent_id: parent.id])}
| extended_attrs
]
to_rendered_content_tag(socket, tag, view, attrs)
end
defp connected_nested_render(parent, socket, view, container, opts, sticky?) do
{to_sign_session, _} = load_session(%{}, opts)
{tag, extended_attrs} = container
session_token = sign_nested_session(parent, socket, view, to_sign_session, sticky?)
attrs = [
{:id, socket.id},
{:data,
[
phx_session: session_token,
phx_static: ""
] ++ if(sticky?, do: [phx_sticky: true], else: [phx_parent_id: parent.id])}
| extended_attrs
]
content_tag(tag, attrs, "")
end
defp to_rendered_content_tag(socket, tag, view, attrs) do
rendered = Phoenix.LiveView.Renderer.to_rendered(socket, view)
{diff, _, _} =
Diff.render(socket, rendered, Diff.new_fingerprints(), Diff.new_components())
content_tag(tag, attrs, Diff.to_iodata(diff))
end
defp content_tag(tag, attrs, content) do
tag = to_string(tag)
{:safe, attrs} = Phoenix.HTML.attributes_escape(attrs)
{:safe, [?<, tag, attrs, ?>, content, ?<, ?/, tag, ?>]}
end
defp load_live!(view_or_component, kind) do
case view_or_component.__live__() do
%{kind: ^kind} = config ->
config
%{kind: other} ->
raise "expected #{inspect(view_or_component)} to be a #{kind}, but it is a #{other}"
end
end
defp lifecycle(%{lifecycle: lifecycle}, %{extra: %{on_mount: on_mount}}) do
%{lifecycle | mount: on_mount ++ lifecycle.mount}
end
defp lifecycle(%{lifecycle: lifecycle}, _) do
lifecycle
end
defp call_mount_and_handle_params!(socket, view, session, params, uri) do
mount_params = if socket.router, do: params, else: :not_mounted_at_router
socket
|> Utils.maybe_call_live_view_mount!(view, mount_params, session, uri)
|> mount_handle_params(view, params, uri)
|> case do
{:noreply, %Socket{redirected: {:live, _, _}} = socket} ->
{:stop, socket}
{:noreply, %Socket{redirected: {:redirect, _opts}} = new_socket} ->
{:stop, new_socket}
{:noreply, %Socket{redirected: nil} = new_socket} ->
{:ok, new_socket}
end
end
defp mount_handle_params(%Socket{redirected: mount_redir} = socket, view, params, uri) do
lifecycle = Lifecycle.stage_info(socket, view, :handle_params, 3)
cond do
mount_redir ->
{:noreply, socket}
not lifecycle.any? ->
{:noreply, socket}
is_nil(socket.router) ->
# Let the callback fail for the usual reasons
Route.live_link_info!(socket, view, uri)
true ->
Utils.call_handle_params!(socket, view, lifecycle.exported?, params, uri)
end
end
defp sign_root_session(%Socket{} = socket, router, view, session, live_session) do
live_session_name =
case live_session do
%{name: name} -> name
nil -> nil
end
# IMPORTANT: If you change the second argument, @token_vsn has to be bumped.
sign_token(socket.endpoint, %{
id: socket.id,
view: view,
root_view: view,
router: router,
live_session_name: live_session_name,
parent_pid: nil,
root_pid: nil,
session: session
})
end
defp sign_nested_session(%Socket{} = parent, %Socket{} = child, view, session, sticky?) do
# IMPORTANT: If you change the second argument, @token_vsn has to be bumped.
sign_token(parent.endpoint, %{
id: child.id,
view: view,
root_view: if(sticky?, do: view, else: parent.private.root_view),
router: parent.router,
parent_pid: if(sticky?, do: nil, else: self()),
root_pid: if(sticky?, do: nil, else: parent.root_pid),
session: session
})
end
# The static token is computed only on disconnected render and it keeps
# the information that is only available during disconnected renders,
# such as assign_new.
defp sign_static_token(%Socket{id: id, endpoint: endpoint} = socket) do
# IMPORTANT: If you change the second argument, @token_vsn has to be bumped.
sign_token(endpoint, %{
id: id,
flash: socket.assigns.flash,
assign_new: assign_new_keys(socket)
})
end
@doc """
Signs a LiveView token.
"""
def sign_token(endpoint, data) do
Phoenix.Token.sign(endpoint, Utils.salt!(endpoint), {@token_vsn, data})
end
defp container(%{container: {tag, attrs}}, opts) do
case opts[:container] do
{tag, extra} -> {tag, Keyword.merge(attrs, extra)}
nil -> {tag, attrs}
end
end
defp assign_new_keys(socket) do
{_, keys} = socket.private.assign_new
keys
end
end