From 2076a6c82609423540b0f4edb0fa1a2f7704d4b5 Mon Sep 17 00:00:00 2001 From: Serhii Fesenko <8006993+SerhiiFesenko@users.noreply.github.com> Date: Wed, 27 May 2026 08:52:07 +0300 Subject: [PATCH 1/3] docs: document hooks and JS commands in dead views --- guides/client/bindings.md | 2 ++ guides/client/js-interop.md | 53 ++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/guides/client/bindings.md b/guides/client/bindings.md index 58c6ba79a0..3e6627bf3d 100644 --- a/guides/client/bindings.md +++ b/guides/client/bindings.md @@ -351,6 +351,7 @@ If `phx-mounted` is used on the initial page render, it will run at the earliest opportunity. For elements outside of a LiveView, this is as soon as `liveSocket.connect()` is executed. For elements inside of a LiveView, this is only after the initial socket connection is established and the LiveView is mounted. +See [Hooks and JS commands outside of a LiveView](js-interop.md#hooks-and-js-commands-outside-of-a-liveview). To react to elements being removed from the DOM, the `phx-remove` binding may be specified, which can contain a `Phoenix.LiveView.JS` command to execute. @@ -376,6 +377,7 @@ recovers: `phx-connected` and `phx-disconnected` are only executed when operating inside a LiveView container. For static templates, they will have no effect. +See [Hooks and JS commands outside of a LiveView](js-interop.md#hooks-and-js-commands-outside-of-a-liveview). ## LiveView events prefix diff --git a/guides/client/js-interop.md b/guides/client/js-interop.md index 4c7b2f7036..1ab04b2cc9 100644 --- a/guides/client/js-interop.md +++ b/guides/client/js-interop.md @@ -159,10 +159,11 @@ or removed by the server, a hook object may be provided via `phx-hook`. * `disconnected` - the element's parent LiveView has disconnected from the server * `reconnected` - the element's parent LiveView has reconnected to the server -*Note:* When using hooks outside the context of a LiveView, `mounted` is the only -callback invoked, and only those elements on the page at DOM ready will be tracked. -For dynamic tracking of the DOM as elements are added, removed, and updated, a LiveView -should be used. +*Note:* hooks also run on regular pages that are *not* LiveViews — see +[Hooks and JS commands outside of a LiveView](#hooks-and-js-commands-outside-of-a-liveview). +In that case, `mounted` is the only callback invoked, and only those elements on the page +at DOM ready will be tracked. For dynamic tracking of the DOM as elements are added, +removed, and updated, a LiveView should be used. The above life-cycle callbacks have in-scope access to the following attributes: @@ -498,3 +499,47 @@ Hooks.MyHook = { Setting IDs directly via `node.id = "..."` or other direct DOM manipulation methods will cause DOM patching issues. Always use `js().setAttribute()` instead. If the server has already assigned an ID to an element, you cannot replace it with a different ID from the client side. Client-side IDs should only be set on elements that have no server-assigned ID. + +## Hooks and JS commands outside of a LiveView + +Hooks (`phx-hook`) and `Phoenix.LiveView.JS` commands are not exclusive to LiveViews. +They also work on regular pages rendered by a normal Phoenix controller — pages with no +live connection, commonly called **dead views** (also referred to as *dead renders*, +*static pages*, or simply markup *outside of a LiveView*). + +This includes markup that lives *outside* the live container on a page that does have a +LiveView, such as your root layout — those elements belong to a body-level dead view, not +to the LiveView. + +To enable it, the page must load and connect `LiveSocket`, exactly as a LiveView page does: + +```javascript +import {LiveSocket} from "phoenix_live_view" + +const liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks}) +liveSocket.connect() +``` + +### What works in a dead view + + * **`phx-hook`** — the `mounted` callback runs, and only for elements present at DOM + ready. The other callbacks (`updated`, `beforeUpdate`, `destroyed`, `disconnected`, + `reconnected`) are never invoked, because a dead view receives no updates from a server. + * **`phx-mounted`** — runs as soon as `liveSocket.connect()` is executed + (see [Bindings](bindings.md#dom-patching)). + * **`phx-click`** and other event bindings that trigger **purely client-side `JS` + commands** — for example `JS.toggle/1`, `JS.show/1`, `JS.hide/1`, `JS.add_class/1`, + `JS.dispatch/1`, and `JS.transition/1`. These execute entirely in the browser, so they + work with no server round-trip. + * **`JS.navigate/1`** and **`JS.patch/1`** — if the page has a connected LiveView (for + example a link in the root layout that sits outside the live container), they perform + normal live navigation against it. On a fully static page with no LiveView, they + gracefully fall back to a full-page browser navigation to the target URL. + +### What does not work in a dead view + + * Anything that needs a connected LiveView: `JS.push/1`, form bindings (`phx-change`, + `phx-submit`), and other event bindings that push to the server have no LiveView + process to reach, so they have no effect. + * The `phx-connected` and `phx-disconnected` bindings — they only take effect inside a + LiveView container and have no effect on a dead view. From b4e3eb947736a028c366f78b91a86bd7aec8e834 Mon Sep 17 00:00:00 2001 From: Serhii Fesenko <8006993+SerhiiFesenko@users.noreply.github.com> Date: Wed, 27 May 2026 12:49:56 +0300 Subject: [PATCH 2/3] docs: address review feedback on regular view section --- guides/client/js-interop.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/guides/client/js-interop.md b/guides/client/js-interop.md index 1ab04b2cc9..3c6ca66cb1 100644 --- a/guides/client/js-interop.md +++ b/guides/client/js-interop.md @@ -503,13 +503,14 @@ If the server has already assigned an ID to an element, you cannot replace it wi ## Hooks and JS commands outside of a LiveView Hooks (`phx-hook`) and `Phoenix.LiveView.JS` commands are not exclusive to LiveViews. -They also work on regular pages rendered by a normal Phoenix controller — pages with no -live connection, commonly called **dead views** (also referred to as *dead renders*, +They also work on regular pages [rendered by a normal Phoenix +controller](https://hexdocs.pm/phoenix/controllers.html#rendering) — pages with no +live connection (sometimes referred to as **dead views**, *dead renders*, *static pages*, or simply markup *outside of a LiveView*). This includes markup that lives *outside* the live container on a page that does have a -LiveView, such as your root layout — those elements belong to a body-level dead view, not -to the LiveView. +LiveView, such as your root layout — those elements belong to a body-level regular view, +not to the LiveView. To enable it, the page must load and connect `LiveSocket`, exactly as a LiveView page does: @@ -520,13 +521,13 @@ const liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks}) liveSocket.connect() ``` -### What works in a dead view +### What works in a regular view * **`phx-hook`** — the `mounted` callback runs, and only for elements present at DOM ready. The other callbacks (`updated`, `beforeUpdate`, `destroyed`, `disconnected`, - `reconnected`) are never invoked, because a dead view receives no updates from a server. - * **`phx-mounted`** — runs as soon as `liveSocket.connect()` is executed - (see [Bindings](bindings.md#dom-patching)). + `reconnected`) are never invoked, because a regular view receives no updates from a server. + * **`phx-mounted`** — runs once the document is ready (`DOMContentLoaded`) and + `liveSocket.connect()` has been called (see [Bindings](bindings.md#dom-patching)). * **`phx-click`** and other event bindings that trigger **purely client-side `JS` commands** — for example `JS.toggle/1`, `JS.show/1`, `JS.hide/1`, `JS.add_class/1`, `JS.dispatch/1`, and `JS.transition/1`. These execute entirely in the browser, so they @@ -536,10 +537,10 @@ liveSocket.connect() normal live navigation against it. On a fully static page with no LiveView, they gracefully fall back to a full-page browser navigation to the target URL. -### What does not work in a dead view +### What does not work in a regular view * Anything that needs a connected LiveView: `JS.push/1`, form bindings (`phx-change`, `phx-submit`), and other event bindings that push to the server have no LiveView process to reach, so they have no effect. * The `phx-connected` and `phx-disconnected` bindings — they only take effect inside a - LiveView container and have no effect on a dead view. + LiveView container and have no effect on a regular view. From 2be264aca869dc82d914c4b91e20b5458725c5e2 Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Fri, 29 May 2026 08:38:05 +0200 Subject: [PATCH 3/3] Update guides/client/js-interop.md --- guides/client/js-interop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/client/js-interop.md b/guides/client/js-interop.md index 3c6ca66cb1..3047249d8f 100644 --- a/guides/client/js-interop.md +++ b/guides/client/js-interop.md @@ -505,7 +505,7 @@ If the server has already assigned an ID to an element, you cannot replace it wi Hooks (`phx-hook`) and `Phoenix.LiveView.JS` commands are not exclusive to LiveViews. They also work on regular pages [rendered by a normal Phoenix controller](https://hexdocs.pm/phoenix/controllers.html#rendering) — pages with no -live connection (sometimes referred to as **dead views**, *dead renders*, +live connection (sometimes referred to as *dead views*, *dead renders*, *static pages*, or simply markup *outside of a LiveView*). This includes markup that lives *outside* the live container on a page that does have a