Skip to content

Commit

Permalink
support multiple livesockets
Browse files Browse the repository at this point in the history
Add `rootViewSelector` option to liveSocket constructor so different
liveSockets can target different liveview HTML nodes in the page.
  • Loading branch information
gfrancischelli committed Dec 9, 2024
1 parent d1250dc commit 47f81f6
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 2 deletions.
15 changes: 13 additions & 2 deletions assets/js/phoenix_live_view/live_socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
* @param {Object} [opts.localStorage] - An optional Storage compatible object
* Useful for when LiveView won't have access to `localStorage`.
* See `opts.sessionStorage` for examples.
* @param {string} [opts.rootViewSelector] - The optional CSS selector to scope which root LiveViews to connect.
* Useful when running multiple liveSockets, each connected to a different application.
*/

import {
Expand Down Expand Up @@ -159,6 +161,7 @@ export default class LiveSocket {
this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER
this.localStorage = opts.localStorage || window.localStorage
this.sessionStorage = opts.sessionStorage || window.sessionStorage
this.rootViewSelector = opts.rootViewSelector
this.boundTopLevelEvents = false
this.boundEventNames = new Set()
this.serverCloseRef = null
Expand Down Expand Up @@ -366,9 +369,13 @@ export default class LiveSocket {
}
}

viewSelector() {

Check failure on line 372 in assets/js/phoenix_live_view/live_socket.js

View workflow job for this annotation

GitHub Actions / npm test (1.17.2, 27)

Unexpected space before opening brace
return `${PHX_VIEW_SELECTOR}${this.rootViewSelector || ""}`
}

joinRootViews(){
let rootsFound = false
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, rootEl => {
DOM.all(document, `${this.viewSelector()}:not([${PHX_PARENT_ID}])`, rootEl => {
if(!this.getRootById(rootEl.id)){
let view = this.newRootView(rootEl)
view.setHref(this.getHref())
Expand Down Expand Up @@ -451,7 +458,11 @@ export default class LiveSocket {
}

owner(childEl, callback){
let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), el => this.getViewByEl(el)) || this.main
let view = maybe(childEl.closest(this.viewSelector()), el => this.getViewByEl(el))
// If there's a rootViewSelector, don't default to `this.main`
// since it's not guaranteed to belong to same liveSocket.
// Maybe `this.embbededMode = boolean()` would be a more clear check?
if (!view && !this.rootViewSelector){ view = this.main }

Check failure on line 465 in assets/js/phoenix_live_view/live_socket.js

View workflow job for this annotation

GitHub Actions / npm test (1.17.2, 27)

Unexpected space(s) after "if"
return view && callback ? callback(view) : view
}

Expand Down
45 changes: 45 additions & 0 deletions guides/client/js-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ except for the following LiveView specific options:
* `uploaders` – a reference to a user-defined uploaders namespace, containing
client callbacks for client-side direct-to-cloud uploads. See the
[External uploads guide](external-uploads.md) for details.
* `rootViewSelector` - the optional CSS selector to scope which root LiveViews to connect.
Useful when running multiple liveSockets, each connected to a different application.
See the [Connecting multiple livesockets](#connecting-multiple-livesockets)
section below for details.

a CSS selector to scope which

## Debugging client events

Expand Down Expand Up @@ -313,3 +319,42 @@ Hooks.Chart = {
```
*Note*: In case a LiveView pushes events and renders content, `handleEvent` callbacks are invoked after the page is updated. Therefore, if the LiveView redirects at the same time it pushes events, callbacks won't be invoked on the old page's elements. Callbacks would be invoked on the redirected page's newly mounted hook elements.
### Connecting multiple liveSockets
LiveView allows connecting more than one `liveSocket`, each targeting different HTML nodes. This is useful to
isolate the development cycle of a subset of the user interface. This means a different Phoenix application hosted
in a different domain, can fully support an embedded LiveView. Think of it as Nested LiveViews, but instead of
process-level isolation, it is a service-level isolation.
Annotate your root views with a unique HTML attribute or class:
```elixir
# Main application serving a regular LiveView
use GreatProductWeb.LiveView, container: {:div, "data-app": "root"}

# Cats application, which will serve the cats component
use CatsWeb.LiveView, container: {:div, "data-app": "cats"}
```
And initialise the liveSockets:
```javascript
# Fetch the disconnected render
let disconnectedCatsHTML = await fetch("https://cats.io/live", { credentials: 'include' })
.then((response) => response.text())
.catch((error) => console.error(error));

# Append it to HTML
document.queryElementById("#cats-slot").innerHTML = disconnectedCatsHTML


# Connect main liveSocket
let liveSocket = new LiveSocket("https://root.io/live", Socket, {rootViewSelector: "[data-app='root']"})
liveSocket.connect()

# Connect the cats liveSocket
let liveSocketCats = new LiveSocket("https://cats.io/live", Socket, {rootViewSelector: "[data-app='cats']"})
liveSocketCats.connect()
```

0 comments on commit 47f81f6

Please sign in to comment.