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..ec19180d61 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){ @@ -795,7 +799,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 +810,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..c886cfe6e6 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 @@ -1123,6 +1123,10 @@ defmodule Phoenix.LiveView.Channel do 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 @@ -1164,7 +1168,15 @@ 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( + parent, + root_view, + assign_new, + connect_params, + connect_info, + lifecycle, + handover_pid + ) do {:ok, mount_priv} -> socket = Utils.configure_socket(socket, mount_priv, action, flash, host_uri) @@ -1254,7 +1266,15 @@ defmodule Phoenix.LiveView.Channel do socket end - defp mount_private(nil, root_view, assign_new, connect_params, connect_info, lifecycle) do + defp mount_private( + nil, + root_view, + assign_new, + connect_params, + connect_info, + lifecycle, + handover_pid + ) do {:ok, %{ connect_params: connect_params, @@ -1262,12 +1282,21 @@ defmodule Phoenix.LiveView.Channel do 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( + parent, + root_view, + assign_new, + connect_params, + connect_info, + lifecycle, + _handover_pid + ) do + case get_assigns(parent, assign_new) do {:ok, parent_assigns} -> # Child live views always ignore the layout on `:use`. {:ok, @@ -1278,7 +1307,8 @@ defmodule Phoenix.LiveView.Channel do live_layout: false, lifecycle: lifecycle, root_view: root_view, - live_temp: %{} + live_temp: %{}, + handover_pid: nil }} {:error, :noproc} -> @@ -1286,9 +1316,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 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..7ea2242574 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 @@ -73,6 +74,7 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.ALive do def render(assigns) do ~H"""
Foo: <%= @foo %>
Current param: <%= @param_current %>
@@ -100,6 +102,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 @@ -128,6 +131,7 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.BLive do def render(assigns) do ~H"""Foo: <%= @foo %>
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.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.getByText("Foo:")).toContainText("baz"); +}); 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"}, []})