Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add auto_connect option to LiveView mount options #3704

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/js/phoenix_live_view/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const PHX_ERROR_CLASS = "phx-error"
export const PHX_CLIENT_ERROR_CLASS = "phx-client-error"
export const PHX_SERVER_ERROR_CLASS = "phx-server-error"
export const PHX_PARENT_ID = "data-phx-parent-id"
export const PHX_AUTO_CONNECT = "data-phx-auto-connect"
export const PHX_MAIN = "data-phx-main"
export const PHX_ROOT_ID = "data-phx-root-id"
export const PHX_VIEWPORT_TOP = "viewport-top"
Expand Down
3 changes: 2 additions & 1 deletion assets/js/phoenix_live_view/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import {
PHX_PARENT_ID,
PHX_VIEW_SELECTOR,
PHX_ROOT_ID,
PHX_AUTO_CONNECT,
PHX_THROTTLE,
PHX_TRACK_UPLOADS,
PHX_SESSION,
Expand Down Expand Up @@ -372,7 +373,7 @@ export default class LiveSocket {

joinRootViews(){
let rootsFound = false
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, rootEl => {
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}]):not([${PHX_AUTO_CONNECT}="false"])`, rootEl => {
if(!this.getRootById(rootEl.id)){
let view = this.newRootView(rootEl)
// stickies cannot be mounted at the router and therefore should not
Expand Down
7 changes: 7 additions & 0 deletions lib/phoenix_live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ defmodule Phoenix.LiveView do
this option will override any layout previously set via
`Phoenix.LiveView.Router.live_session/2` or on `use Phoenix.LiveView`

* `:auto_connect` - if false, instructs the LiveView JavaScript client
to not automatically connect to the server on disconnected render.
This is useful when you have a static page that does not require
any connected functionality, but should render over the existing
connection when navigating from an already connected LiveView.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A LiveView with auto_connect: false is special in that its reactivity would depend whether you arrived over a regular HTTP request or live navigation.

What would happen if the socket disconnects for some reason (e.g. user drives through a tunnel or server is redeployed)?

Defaults to `true`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what happens if auto_connect is false and I go to a page also with auto_connect=false, trying to use patch or navigate? I assume it does a full page navigation anyway? We should document this.


"""
@callback mount(
params :: unsigned_params() | :not_mounted_at_router,
Expand Down
6 changes: 6 additions & 0 deletions lib/phoenix_live_view/static.ex
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ defmodule Phoenix.LiveView.Static do

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

data_attrs =
if(not Map.get(socket.private, :auto_connect, true),
do: [phx_auto_connect: "false"],
else: []
) ++ data_attrs

attrs = [
{:id, socket.id},
{:data, data_attrs}
Expand Down
10 changes: 9 additions & 1 deletion lib/phoenix_live_view/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Phoenix.LiveView.Utils do
alias Phoenix.LiveView.{Socket, Lifecycle}

# All available mount options
@mount_opts [:temporary_assigns, :layout]
@mount_opts [:temporary_assigns, :layout, :auto_connect]

@max_flash_age :timer.seconds(60)

Expand Down Expand Up @@ -437,6 +437,14 @@ defmodule Phoenix.LiveView.Utils do
}
end

defp handle_mount_option(%Socket{} = socket, :auto_connect, value) do
if not is_boolean(value) do
raise "the :auto_connect mount option must be a boolean, got: #{inspect(value)}"
end

put_in(socket.private[:auto_connect], value)
end

@doc """
Calls the `handle_params/3` callback, and returns the result.

Expand Down
23 changes: 23 additions & 0 deletions test/e2e/support/lifecycle.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Phoenix.LiveViewTest.E2E.LifecycleLive do
use Phoenix.LiveView

@impl Phoenix.LiveView
def mount(params, _session, socket) do
auto_connect =
case params do
%{"auto_connect" => "false"} -> false
_ -> true
end

{:ok, socket, auto_connect: auto_connect}
end

@impl Phoenix.LiveView
def render(assigns) do
~H"""
<div>Hello!</div>
<.link navigate="/lifecycle">Navigate to self (auto_connect=true)</.link>
<.link navigate="/lifecycle?auto_connect=false">Navigate to self (auto_connect=false)</.link>
"""
end
end
1 change: 1 addition & 0 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
live "/form/stream", E2E.FormStreamLive
live "/js", E2E.JsLive
live "/select", E2E.SelectLive
live "/lifecycle", E2E.LifecycleLive
end

scope "/issues", Phoenix.LiveViewTest.E2E do
Expand Down
57 changes: 57 additions & 0 deletions test/e2e/tests/lifecycle.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const {test, expect} = require("../test-fixtures")
const {syncLV} = require("../utils")

test.describe("auto_connect", () => {
let webSocketEvents = []
let networkEvents = []
let consoleMessages = []

test.beforeEach(async ({page}) => {
networkEvents = []
webSocketEvents = []
consoleMessages = []

page.on("request", request => networkEvents.push({method: request.method(), url: request.url()}))

page.on("websocket", ws => {
ws.on("framesent", event => webSocketEvents.push({type: "sent", payload: event.payload}))
ws.on("framereceived", event => webSocketEvents.push({type: "received", payload: event.payload}))
ws.on("close", () => webSocketEvents.push({type: "close"}))
})

page.on("console", msg => consoleMessages.push(msg.text()))
})

test("connects by default", async ({page}) => {
await page.goto("/lifecycle")
await syncLV(page)

expect(webSocketEvents).toHaveLength(2)
})

test("does not connect when auto_connect is false", async ({page}) => {
await page.goto("/lifecycle?auto_connect=false")
// eslint-disable-next-line playwright/no-networkidle
await page.waitForLoadState("networkidle")
expect(webSocketEvents).toHaveLength(0)
})

test("connects when navigating to a view with auto_connect=true", async ({page}) => {
await page.goto("/lifecycle?auto_connect=false")
// eslint-disable-next-line playwright/no-networkidle
await page.waitForLoadState("networkidle")
expect(webSocketEvents).toHaveLength(0)
await page.getByRole("link", {name: "Navigate to self (auto_connect=true)"}).click()
await syncLV(page)
expect(webSocketEvents).toHaveLength(2)
})

test("stays connected when navigating to a view with auto_connect=false", async ({page}) => {
await page.goto("/lifecycle")
await syncLV(page)
expect(webSocketEvents.filter(e => e.payload.includes("phx_join"))).toHaveLength(1)
await page.getByRole("link", {name: "Navigate to self (auto_connect=false)"}).click()
await syncLV(page)
expect(webSocketEvents.filter(e => e.payload.includes("phx_join"))).toHaveLength(2)
})
})
Loading