diff --git a/assets/js/phoenix_live_view/live_socket.js b/assets/js/phoenix_live_view/live_socket.js index 1b984eb87e..82c34997ff 100644 --- a/assets/js/phoenix_live_view/live_socket.js +++ b/assets/js/phoenix_live_view/live_socket.js @@ -390,7 +390,7 @@ export default class LiveSocket { let removeEls = DOM.all(this.outgoingMainEl, `[${this.binding("remove")}]`) let newMainEl = DOM.cloneNode(this.outgoingMainEl, "") this.main.showLoader(this.loaderTimeout) - this.main.destroy() + this.main.destroy(false) this.main = this.newRootView(newMainEl, flash, liveReferer) this.main.setRedirect(href) @@ -407,7 +407,7 @@ export default class LiveSocket { onDone() }) } - }) + }, true) } transitionRemoves(elements, skipSticky, callback){ diff --git a/assets/js/phoenix_live_view/view.js b/assets/js/phoenix_live_view/view.js index ff6f814fd6..70f3a3512a 100644 --- a/assets/js/phoenix_live_view/view.js +++ b/assets/js/phoenix_live_view/view.js @@ -205,7 +205,7 @@ export default class View { return val === "" ? null : val } - destroy(callback = function (){ }){ + destroy(leave = true, callback = function (){ }){ this.destroyAllChildren() this.destroyed = true delete this.root.children[this.id] @@ -221,10 +221,14 @@ export default class View { DOM.markPhxChildDestroyed(this.el) this.log("destroyed", () => ["the child has been removed from the parent"]) - this.channel.leave() - .receive("ok", onFinished) - .receive("error", onFinished) - .receive("timeout", onFinished) + if(leave){ + this.channel.leave() + .receive("ok", onFinished) + .receive("error", onFinished) + .receive("timeout", onFinished) + } else { + onFinished() + } } setContainerClasses(...classes){ @@ -761,6 +765,8 @@ export default class View { this.onChannel("redirect", ({to, flash}) => this.onRedirect({to, flash})) this.onChannel("live_patch", (redir) => this.onLivePatch(redir)) this.onChannel("live_redirect", (redir) => this.onLiveRedirect(redir)) + this.onChannel("live_handover", (redir) => this.startHandover(redir)) + this.onChannel("phx_handover", (payload) => this.completeHandover(payload)) this.channel.onError(reason => this.onError(reason)) this.channel.onClose(reason => this.onClose(reason)) } @@ -786,6 +792,44 @@ export default class View { onRedirect({to, flash, reloadToken}){ this.liveSocket.redirect(to, flash, reloadToken) } + startHandover(redir){ + if(!this.isMain()){ + throw new Error("unexpected handover for non main view") + } + let {to, kind, _flash} = redir + let href = this.expandURL(to) + let scroll = window.scrollY + this.liveSocket.withPageLoading({to: href, kind}, done => { + // let liveReferer = this.currentLocation.href + let removeEls = DOM.all(this.el, `[${this.binding("remove")}]`) + let newMainEl = DOM.cloneNode(this.el, "") + this.outGoingEl = this.el + this.el = newMainEl + this.showLoader(this.liveSocket.loaderTimeout) + + this.setRedirect(href) + this.liveSocket.transitionRemoves(removeEls, true) + this.handoverCallback = () => { + this.stopCallback = function(){} + this.liveSocket.requestDOMUpdate(() => { + // remove phx-remove els right before we replace the main element + removeEls.forEach(el => el.remove()) + DOM.findPhxSticky(document).forEach(el => newMainEl.appendChild(el)) + this.outGoingEl.replaceWith(this.el) + Browser.pushState(kind, {type: "redirect", id: this.id, scroll: scroll}, href) + DOM.dispatchEvent(window, "phx:navigate", {detail: {href, patch: false, pop: false}}) + this.liveSocket.registerNewLocation(window.location) + done() + }) + } + }) + } + + completeHandover(payload){ + this.stopCallback = this.handoverCallback + this.onJoin(payload) + } + isDestroyed(){ return this.destroyed } joinDead(){ this.isDead = true } @@ -795,7 +839,7 @@ export default class View { return this.joinPush } - join(callback){ + join(callback, handover = false){ this.showLoader(this.liveSocket.loaderTimeout) this.bindChannel() if(this.isMain()){ @@ -806,7 +850,7 @@ export default class View { callback ? callback(this.joinCount, onDone) : onDone() } - this.wrapPush(() => this.channel.join(), { + this.wrapPush(() => this.channel.join(this.liveSocket.socket.timeout, handover), { ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)), error: (error) => this.onJoinError(error), timeout: () => this.onJoinError({reason: "timeout"}) diff --git a/lib/phoenix_live_view/channel.ex b/lib/phoenix_live_view/channel.ex index 696933d0ef..099e40b56b 100644 --- a/lib/phoenix_live_view/channel.ex +++ b/lib/phoenix_live_view/channel.ex @@ -399,7 +399,7 @@ defmodule Phoenix.LiveView.Channel do end) end - def handle_call({@prefix, :child_mount, _child_pid, assign_new}, _from, state) do + def handle_call({@prefix, :get_assigns, _child_pid, assign_new}, _from, state) do assigns = Map.take(state.socket.assigns, assign_new) {:reply, {:ok, assigns}, state} end @@ -848,9 +848,12 @@ defmodule Phoenix.LiveView.Channel do opts = copy_flash(new_state, flash, opts) new_state - |> push_pending_events_on_redirect(new_socket) - |> push_live_redirect(opts, ref) - |> stop_shutdown_redirect(:live_redirect, opts) + |> maybe_handover(opts, ref, fn new_state -> + new_state + |> push_pending_events_on_redirect(new_socket) + |> push_live_redirect(opts, ref) + |> stop_shutdown_redirect(:live_redirect, opts) + end) {:live, :patch, %{to: _to, kind: _kind} = opts} when root_pid == self() -> {params, action} = patch_params_and_action!(new_socket, opts) @@ -1079,6 +1082,34 @@ defmodule Phoenix.LiveView.Channel do end end + # handover from previous LV in the same live session + defp mount({:handover, new_verified, route, url, params}, from, phx_socket) + when is_map_key(phx_socket, :handover_pid) and is_pid(phx_socket.handover_pid) and + phx_socket.handover_pid == new_verified.root_pid do + %Phoenix.Socket{private: %{connect_info: connect_info}} = phx_socket + %Session{view: view} = new_verified + + new_verified = %{new_verified | root_pid: self()} + + case load_live_view(view) do + {:ok, config} -> + verified_mount( + new_verified, + config, + route, + url, + params, + from, + phx_socket, + connect_info + ) + + {:error, _reason} -> + GenServer.reply(from, {:error, %{reason: "stale"}}) + {:stop, :shutdown, :no_state} + end + end + defp mount(%{}, from, phx_socket) do Logger.error("Mounting #{phx_socket.topic} failed because no session was provided") GenServer.reply(from, {:error, %{reason: "stale"}}) @@ -1098,7 +1129,7 @@ defmodule Phoenix.LiveView.Channel do end defp verified_mount( - %Session{} = verified, + %Session{} = verified_session, config, route, url, @@ -1110,19 +1141,21 @@ defmodule Phoenix.LiveView.Channel do %Session{ id: id, view: view, - root_view: root_view, parent_pid: parent, root_pid: root_pid, session: verified_user_session, - assign_new: assign_new, router: router - } = verified + } = verified_session %Phoenix.Socket{ endpoint: endpoint, transport_pid: transport_pid } = phx_socket + # TODO: change this to directly pattern match on handover_pid above + # when we require Phoenix 1.8 + handover_pid = Map.get(phx_socket, :handover_pid) + Process.put(:"$initial_call", {view, :mount, 3}) case params do @@ -1134,7 +1167,7 @@ defmodule Phoenix.LiveView.Channel do connect_params = params["params"] # Optional verified parts - flash = verify_flash(endpoint, verified, params["flash"], connect_params) + flash = verify_flash(endpoint, verified_session, params["flash"], connect_params) # connect_info is either a Plug.Conn during tests or a Phoenix.Socket map socket_session = Map.get(connect_info, :session, %{}) @@ -1164,7 +1197,13 @@ defmodule Phoenix.LiveView.Channel do merged_session = Map.merge(socket_session, verified_user_session) lifecycle = load_lifecycle(config, route) - case mount_private(parent, root_view, assign_new, connect_params, connect_info, lifecycle) do + case mount_private( + verified_session, + connect_params, + connect_info, + lifecycle, + handover_pid + ) do {:ok, mount_priv} -> socket = Utils.configure_socket(socket, mount_priv, action, flash, host_uri) @@ -1174,7 +1213,7 @@ defmodule Phoenix.LiveView.Channel do |> Utils.maybe_call_live_view_mount!(view, params, merged_session, url) |> build_state(phx_socket) |> maybe_call_mount_handle_params(router, url, params) - |> reply_mount(from, verified, route) + |> reply_mount(from, verified_session, route) |> maybe_subscribe_to_live_reload() rescue exception -> @@ -1254,31 +1293,49 @@ defmodule Phoenix.LiveView.Channel do socket end - defp mount_private(nil, root_view, assign_new, connect_params, connect_info, lifecycle) do + defp mount_private( + %Session{parent_pid: nil, root_view: root_view, assign_new: assign_new} = + verified_session, + connect_params, + connect_info, + lifecycle, + handover_pid + ) do {:ok, %{ + verified_session: verified_session, connect_params: connect_params, connect_info: connect_info, assign_new: {%{}, assign_new}, lifecycle: lifecycle, root_view: root_view, - live_temp: %{} + live_temp: %{}, + handover_pid: handover_pid }} end - defp mount_private(parent, root_view, assign_new, connect_params, connect_info, lifecycle) do - case sync_with_parent(parent, assign_new) do + defp mount_private( + %Session{parent_pid: parent_pid, root_view: root_view, assign_new: assign_new} = + verified_session, + connect_params, + connect_info, + lifecycle, + _handover_pid + ) do + case get_assigns(parent_pid, assign_new) do {:ok, parent_assigns} -> # Child live views always ignore the layout on `:use`. {:ok, %{ + verified_session: verified_session, connect_params: connect_params, connect_info: connect_info, assign_new: {parent_assigns, assign_new}, live_layout: false, lifecycle: lifecycle, root_view: root_view, - live_temp: %{} + live_temp: %{}, + handover_pid: nil }} {:error, :noproc} -> @@ -1286,9 +1343,9 @@ defmodule Phoenix.LiveView.Channel do end end - defp sync_with_parent(parent, assign_new) do + def get_assigns(pid, keys) do try do - GenServer.call(parent, {@prefix, :child_mount, self(), assign_new}) + GenServer.call(pid, {@prefix, :get_assigns, self(), keys}) catch :exit, {:noproc, _} -> {:error, :noproc} end @@ -1336,8 +1393,10 @@ defmodule Phoenix.LiveView.Channel do {:noreply, post_verified_mount(new_state)} {:live_redirect, opts, new_state} -> - GenServer.reply(from, {:error, %{live_redirect: opts}}) - {:stop, :shutdown, new_state} + maybe_handover(new_state, opts, nil, fn new_state -> + GenServer.reply(from, {:error, %{live_redirect: opts}}) + {:stop, :shutdown, new_state} + end) {:redirect, opts, new_state} -> GenServer.reply(from, {:error, %{redirect: opts}}) @@ -1579,4 +1638,40 @@ defmodule Phoenix.LiveView.Channel do %{} end end + + defp handover? do + phoenix_vsn = to_string(Application.spec(:phoenix)[:vsn]) + Version.match?(phoenix_vsn, ">= 1.8.0-dev") + end + + defp maybe_handover(state, redirect_opts, ref, fallback) do + %{socket: %{parent_pid: parent, private: %{verified_session: session}} = socket} = state + %{to: to} = redirect_opts + # get the full uri to verify the new session + destructure [path, query], :binary.split(to, "?") + to = %{socket.host_uri | path: path, query: query} + params = (query && Plug.Conn.Query.decode(query)) || %{} + + if diff = Diff.get_push_events_diff(socket), do: push_diff(state, diff, ref) + + # we can only handover on Phoenix >= 1.8.0 and when we are mounted at the router + with true <- handover?(), + nil <- parent, + {:ok, new_verified, route, url} <- + authorize_session( + session, + socket.endpoint, + %{"redirect" => to} + ) do + %{topic: topic, join_ref: join_ref} = state + state = push(state, "live_handover", redirect_opts) + + msg_payload = {:handover, new_verified, route, url, params} + send(socket.transport_pid, {:handover, msg_payload, self(), topic, join_ref}) + + {:noreply, state} + else + _ -> fallback.(state) + end + end end diff --git a/lib/phoenix_live_view/socket.ex b/lib/phoenix_live_view/socket.ex index 55bfcc5037..a7a3074513 100644 --- a/lib/phoenix_live_view/socket.ex +++ b/lib/phoenix_live_view/socket.ex @@ -96,7 +96,7 @@ defmodule Phoenix.LiveView.Socket do } channel "lvu:*", Phoenix.LiveView.UploadChannel - channel "lv:*", Phoenix.LiveView.Channel + channel "lv:*", Phoenix.LiveView.Channel, handover_on_rejoin: true @impl Phoenix.Socket def connect(_params, %Phoenix.Socket{} = socket, connect_info) do @@ -111,7 +111,7 @@ defmodule Phoenix.LiveView.Socket do use Phoenix.Socket channel "lvu:*", Phoenix.LiveView.UploadChannel - channel "lv:*", Phoenix.LiveView.Channel + channel "lv:*", Phoenix.LiveView.Channel, handover_on_rejoin: true def connect(params, socket, info), do: {:ok, socket} defdelegate id(socket), to: unquote(__MODULE__) diff --git a/lib/phoenix_live_view/utils.ex b/lib/phoenix_live_view/utils.ex index 50ae88dfbf..d6643c4aa9 100644 --- a/lib/phoenix_live_view/utils.ex +++ b/lib/phoenix_live_view/utils.ex @@ -48,7 +48,7 @@ defmodule Phoenix.LiveView.Utils do %{assigns: %{^key => _}} -> socket - %{private: %{assign_new: {assigns, keys}}} -> + %{private: %{assign_new: {assigns, keys}} = private} -> # It is important to store the keys even if they are not in assigns # because maybe the controller doesn't have it but the view does. socket = put_in(socket.private.assign_new, {assigns, [key | keys]}) @@ -58,7 +58,7 @@ defmodule Phoenix.LiveView.Utils do key, case assigns do %{^key => value} -> value - %{} -> fun.(socket.assigns) + %{} -> maybe_handover_assign(socket, key, private[:handover_pid], fun) end ) @@ -72,17 +72,36 @@ defmodule Phoenix.LiveView.Utils do %{assigns: %{^key => _}} -> socket - %{private: %{assign_new: {assigns, keys}}} -> + %{private: %{assign_new: {assigns, keys}} = private} -> # It is important to store the keys even if they are not in assigns # because maybe the controller doesn't have it but the view does. socket = put_in(socket.private.assign_new, {assigns, [key | keys]}) - Phoenix.LiveView.Utils.force_assign(socket, key, Map.get_lazy(assigns, key, fun)) + + Phoenix.LiveView.Utils.force_assign( + socket, + key, + Map.get_lazy(assigns, key, fn -> + maybe_handover_assign(socket, key, private[:handover_pid], fun) + end) + ) %{} -> Phoenix.LiveView.Utils.force_assign(socket, key, fun.()) end end + defp maybe_handover_assign(_socket, _key, nil, fun) when is_function(fun, 0), do: fun.() + + defp maybe_handover_assign(socket, _key, nil, fun) when is_function(fun, 1), + do: fun.(socket.assigns) + + defp maybe_handover_assign(socket, key, pid, fun) when is_pid(pid) do + case Phoenix.LiveView.Channel.get_assigns(pid, [key]) do + {:ok, %{^key => value}} -> value + _ -> maybe_handover_assign(socket, key, nil, fun) + end + end + @doc """ Forces an assign on a socket. """ @@ -194,7 +213,7 @@ defmodule Phoenix.LiveView.Utils do socket |> clear_changed() |> clear_temp() - |> drop_private([:connect_info, :connect_params, :assign_new]) + |> drop_private([:connect_info, :connect_params, :assign_new, :handover_pid]) end @doc """ diff --git a/mix.exs b/mix.exs index 4a322cce9e..01eb507c3d 100644 --- a/mix.exs +++ b/mix.exs @@ -41,7 +41,10 @@ defmodule Phoenix.LiveView.MixProject do defp deps do [ - {:phoenix, "~> 1.6.15 or ~> 1.7.0"}, + # {:phoenix, "~> 1.6.15 or ~> 1.7.0"}, + # TODO: remove before merging + {:phoenix, + github: "phoenixframework/phoenix", branch: "sd-handover-assigns", override: true}, {:plug, "~> 1.15"}, {:phoenix_template, "~> 1.0"}, {:phoenix_html, "~> 3.3 or ~> 4.0 or ~> 4.1"}, diff --git a/mix.lock b/mix.lock index 8ea34391a7..4917956bc0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, - "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, + "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "ecto": {:hex, :ecto, "3.12.1", "626765f7066589de6fa09e0876a253ff60c3d00870dd3a1cd696e2ba67bfceea", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df0045ab9d87be947228e05a8d153f3e06e0d05ab10c3b3cc557d2f7243d1940"}, @@ -19,7 +19,7 @@ "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "58cae7e9ca0e53bdc96c445210ce3f11bfdd5dce", [branch: "sd-handover-assigns"]}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, @@ -29,7 +29,7 @@ "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, diff --git a/test/e2e/support/navigation.ex b/test/e2e/support/navigation.ex index fc6ceaf32c..b4a07fb347 100644 --- a/test/e2e/support/navigation.ex +++ b/test/e2e/support/navigation.ex @@ -56,6 +56,7 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.ALive do def mount(_params, _session, socket) do socket |> assign(:param_current, nil) + |> assign_new(:foo, fn -> "bar" end) |> then(&{:ok, &1}) end @@ -69,16 +70,24 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.ALive do |> then(&{:noreply, &1}) end + @impl Phoenix.LiveView + def handle_event("push_navigate", _params, socket) do + {:noreply, push_navigate(socket, to: "/navigation/b")} + end + @impl Phoenix.LiveView def render(assigns) do ~H"""
Foo: <%= @foo %>
Current param: <%= @param_current %>
<.styled_link patch={"/navigation/a?param=#{@param_next}"}>Patch this LiveView <.styled_link patch={"/navigation/a?param=#{@param_next}"} replace>Patch (Replace) <.styled_link navigate="/navigation/b#items-item-42">Navigate to 42 + + <.styled_link phx-click="push_navigate">push_navigate """ end @@ -100,6 +109,7 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.BLive do @impl Phoenix.LiveView def mount(_params, _session, socket) do socket + |> assign_new(:foo, fn -> "baz" end) |> then(&{:ok, &1}) end @@ -124,10 +134,24 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.BLive do assign(socket, :id, id) end + @impl Phoenix.LiveView + def handle_event("push_navigate", _params, socket) do + {:noreply, push_navigate(socket, to: "/navigation/a")} + end + @impl Phoenix.LiveView def render(assigns) do ~H"""Foo: <%= @foo %>
+ + + push_navigate + e.payload.indexOf("phx_leave") !== -1)).toHaveLength(0); + await expect(webSocketEvents.filter(e => e.payload.indexOf("phx_join") !== -1)).toHaveLength(1); // we patched 2 times await expect(webSocketEvents.filter(e => e.payload.indexOf("live_patch") !== -1)).toHaveLength(2); }); + +test("sharing assigns between live navigation", async ({ page }) => { + await page.goto("/navigation/a"); + await syncLV(page); + + await expect(page.getByText("Foo:")).toContainText("bar"); + await page.getByRole("link", { name: "LiveView B" }).click(); + await syncLV(page); + await expect(page).toHaveURL("/navigation/b"); + await expect(page.getByText("Foo:")).toContainText("bar"); + + await page.reload(); + await syncLV(page); + await expect(page.getByText("Foo:")).toContainText("baz"); + await page.getByRole("link", { name: "LiveView A" }).click(); + await syncLV(page); + await expect(page).toHaveURL("/navigation/a"); + await expect(page.getByText("Foo:")).toContainText("baz"); +}); + +test("sharing assigns between live navigation (push_navigate)", async ({ page }) => { + await page.goto("/navigation/a"); + await syncLV(page); + + await expect(page.getByText("Foo:")).toContainText("bar"); + await page.getByRole("link", { name: "push_navigate" }).click(); + await syncLV(page); + await expect(page).toHaveURL("/navigation/b"); + await expect(page.getByText("Foo:")).toContainText("bar"); + + await page.reload(); + await syncLV(page); + await expect(page.getByText("Foo:")).toContainText("baz"); + await page.getByRole("link", { name: "push_navigate" }).click(); + await syncLV(page); + await expect(page).toHaveURL("/navigation/a"); + + await expect(page.getByText("Foo:")).toContainText("baz"); +}); \ No newline at end of file diff --git a/test/phoenix_component_test.exs b/test/phoenix_component_test.exs index d23e0c8b8e..415b685637 100644 --- a/test/phoenix_component_test.exs +++ b/test/phoenix_component_test.exs @@ -141,6 +141,33 @@ defmodule Phoenix.ComponentUnitTest do } end + test "does handover of previous assigns when handover_pid is present" do + pid = + spawn(fn -> + receive do + {:"$gen_call", {pid, _} = from, {:phoenix, :get_assigns, pid, [:existing]}} -> + GenServer.reply(from, {:ok, %{existing: "existing-handover"}}) + end + end) + + socket = + put_in(@socket.private[:assign_new], {%{}, []}) + |> put_in([Access.key(:private), :handover_pid], pid) + |> assign(existing2: "existing2") + |> assign_new(:existing, fn -> "new-existing" end) + |> assign_new(:existing2, fn -> "new-existing2" end) + |> assign_new(:notexisting, fn -> "new-notexisting" end) + + assert socket.assigns == %{ + existing: "existing-handover", + existing2: "existing2", + notexisting: "new-notexisting", + live_action: nil, + flash: %{}, + __changed__: %{existing: true, notexisting: true, existing2: true} + } + end + test "has access to assigns" do socket = put_in(@socket.private[:assign_new], {%{existing: "existing-parent"}, []})