Skip to content

Commit 107cb2f

Browse files
committed
add auto_connect option to LiveView mount options
to specify that a LiveView should not automatically connect on dead render. When navigated to and there's already a connection established, this option has no effect. See https://elixirforum.com/t/liveview-feature-to-cancel-second-render/67770
1 parent c44c48e commit 107cb2f

File tree

8 files changed

+105
-2
lines changed

8 files changed

+105
-2
lines changed

assets/js/phoenix_live_view/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const PHX_ERROR_CLASS = "phx-error"
3131
export const PHX_CLIENT_ERROR_CLASS = "phx-client-error"
3232
export const PHX_SERVER_ERROR_CLASS = "phx-server-error"
3333
export const PHX_PARENT_ID = "data-phx-parent-id"
34+
export const PHX_AUTO_CONNECT = "data-phx-auto-connect"
3435
export const PHX_MAIN = "data-phx-main"
3536
export const PHX_ROOT_ID = "data-phx-root-id"
3637
export const PHX_VIEWPORT_TOP = "viewport-top"

assets/js/phoenix_live_view/live_socket.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {
9393
PHX_PARENT_ID,
9494
PHX_VIEW_SELECTOR,
9595
PHX_ROOT_ID,
96+
PHX_AUTO_CONNECT,
9697
PHX_THROTTLE,
9798
PHX_TRACK_UPLOADS,
9899
PHX_SESSION,
@@ -372,7 +373,7 @@ export default class LiveSocket {
372373

373374
joinRootViews(){
374375
let rootsFound = false
375-
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, rootEl => {
376+
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}]):not([${PHX_AUTO_CONNECT}="false"])`, rootEl => {
376377
if(!this.getRootById(rootEl.id)){
377378
let view = this.newRootView(rootEl)
378379
// stickies cannot be mounted at the router and therefore should not

lib/phoenix_live_view.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ defmodule Phoenix.LiveView do
217217
this option will override any layout previously set via
218218
`Phoenix.LiveView.Router.live_session/2` or on `use Phoenix.LiveView`
219219
220+
* `:auto_connect` - if false, instructs the LiveView JavaScript client
221+
to not automatically connect to the server on dead render.
222+
This is useful when you have a static page that does not require
223+
any connected functionality, but should render over the existing
224+
connection when navigating from an already connected LiveView.
225+
Defaults to `true`.
226+
220227
"""
221228
@callback mount(
222229
params :: unsigned_params() | :not_mounted_at_router,

lib/phoenix_live_view/static.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ defmodule Phoenix.LiveView.Static do
161161

162162
data_attrs = if(router, do: [phx_main: true], else: []) ++ data_attrs
163163

164+
data_attrs =
165+
if(not Map.get(socket.private, :auto_connect, true),
166+
do: [phx_auto_connect: "false"],
167+
else: []
168+
) ++ data_attrs
169+
164170
attrs = [
165171
{:id, socket.id},
166172
{:data, data_attrs}

lib/phoenix_live_view/utils.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Phoenix.LiveView.Utils do
66
alias Phoenix.LiveView.{Socket, Lifecycle}
77

88
# All available mount options
9-
@mount_opts [:temporary_assigns, :layout]
9+
@mount_opts [:temporary_assigns, :layout, :auto_connect]
1010

1111
@max_flash_age :timer.seconds(60)
1212

@@ -437,6 +437,14 @@ defmodule Phoenix.LiveView.Utils do
437437
}
438438
end
439439

440+
defp handle_mount_option(%Socket{} = socket, :auto_connect, value) do
441+
if not is_boolean(value) do
442+
raise "the :auto_connect mount option must be a boolean, got: #{inspect(value)}"
443+
end
444+
445+
put_in(socket.private[:auto_connect], value)
446+
end
447+
440448
@doc """
441449
Calls the `handle_params/3` callback, and returns the result.
442450

test/e2e/support/lifecycle.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule Phoenix.LiveViewTest.E2E.LifecycleLive do
2+
use Phoenix.LiveView
3+
4+
@impl Phoenix.LiveView
5+
def mount(params, _session, socket) do
6+
auto_connect = case params do
7+
%{"auto_connect" => "false"} -> false
8+
_ -> true
9+
end
10+
11+
{:ok, socket, auto_connect: auto_connect}
12+
end
13+
14+
@impl Phoenix.LiveView
15+
def render(assigns) do
16+
~H"""
17+
<div>Hello!</div>
18+
<.link navigate="/lifecycle">Navigate to self (auto_connect=true)</.link>
19+
<.link navigate="/lifecycle?auto_connect=false">Navigate to self (auto_connect=false)</.link>
20+
"""
21+
end
22+
end

test/e2e/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
139139
live "/form/stream", E2E.FormStreamLive
140140
live "/js", E2E.JsLive
141141
live "/select", E2E.SelectLive
142+
live "/lifecycle", E2E.LifecycleLive
142143
end
143144

144145
scope "/issues", Phoenix.LiveViewTest.E2E do

test/e2e/tests/lifecycle.spec.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const {test, expect} = require("../test-fixtures")
2+
const {syncLV} = require("../utils")
3+
4+
test.describe("auto_connect", () => {
5+
let webSocketEvents = []
6+
let networkEvents = []
7+
let consoleMessages = []
8+
9+
test.beforeEach(async ({page}) => {
10+
networkEvents = []
11+
webSocketEvents = []
12+
consoleMessages = []
13+
14+
page.on("request", request => networkEvents.push({method: request.method(), url: request.url()}))
15+
16+
page.on("websocket", ws => {
17+
ws.on("framesent", event => webSocketEvents.push({type: "sent", payload: event.payload}))
18+
ws.on("framereceived", event => webSocketEvents.push({type: "received", payload: event.payload}))
19+
ws.on("close", () => webSocketEvents.push({type: "close"}))
20+
})
21+
22+
page.on("console", msg => consoleMessages.push(msg.text()))
23+
})
24+
25+
test("connects by default", async ({page}) => {
26+
await page.goto("/lifecycle")
27+
await syncLV(page)
28+
29+
expect(webSocketEvents).toHaveLength(2)
30+
})
31+
32+
test("does not connect when auto_connect is false", async ({page}) => {
33+
await page.goto("/lifecycle?auto_connect=true")
34+
// eslint-disable-next-line playwright/no-networkidle
35+
await page.waitForLoadState("networkidle")
36+
expect(webSocketEvents).toHaveLength(0)
37+
})
38+
39+
test("connects when navigating to a view with auto_connect=true", async ({page}) => {
40+
await page.goto("/lifecycle?auto_connect=false")
41+
// eslint-disable-next-line playwright/no-networkidle
42+
await page.waitForLoadState("networkidle")
43+
expect(webSocketEvents).toHaveLength(0)
44+
await page.getByRole("link", {name: "Navigate to self (auto_connect=true)"}).click()
45+
await syncLV(page)
46+
expect(webSocketEvents).toHaveLength(2)
47+
})
48+
49+
test("stays connected when navigating to a view with auto_connect=false", async ({page}) => {
50+
await page.goto("/lifecycle")
51+
await syncLV(page)
52+
expect(webSocketEvents).toHaveLength(2)
53+
await page.getByRole("link", {name: "Navigate to self (auto_connect=false)"}).click()
54+
await syncLV(page)
55+
expect(webSocketEvents).toHaveLength(7)
56+
})
57+
})

0 commit comments

Comments
 (0)