Skip to content

Commit 85eb3f0

Browse files
support multiple livesockets
Add `rootViewSelector` option to liveSocket constructor so different liveSockets can target different liveview HTML nodes in the page.
1 parent d1250dc commit 85eb3f0

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

assets/js/phoenix_live_view/live_socket.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
* @param {Object} [opts.localStorage] - An optional Storage compatible object
7171
* Useful for when LiveView won't have access to `localStorage`.
7272
* See `opts.sessionStorage` for examples.
73+
* @param {string} [opts.rootViewSelector] - The optional CSS selector to scope which root LiveViews to connect.
74+
* Useful when running multiple liveSockets, each connected to a different application.
7375
*/
7476

7577
import {
@@ -159,6 +161,7 @@ export default class LiveSocket {
159161
this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER
160162
this.localStorage = opts.localStorage || window.localStorage
161163
this.sessionStorage = opts.sessionStorage || window.sessionStorage
164+
this.rootViewSelector = opts.rootViewSelector
162165
this.boundTopLevelEvents = false
163166
this.boundEventNames = new Set()
164167
this.serverCloseRef = null
@@ -366,9 +369,13 @@ export default class LiveSocket {
366369
}
367370
}
368371

372+
viewSelector() {
373+
return `${PHX_VIEW_SELECTOR}${this.rootViewSelector || ""}`
374+
}
375+
369376
joinRootViews(){
370377
let rootsFound = false
371-
DOM.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, rootEl => {
378+
DOM.all(document, `${this.viewSelector()}:not([${PHX_PARENT_ID}])`, rootEl => {
372379
if(!this.getRootById(rootEl.id)){
373380
let view = this.newRootView(rootEl)
374381
view.setHref(this.getHref())
@@ -451,8 +458,12 @@ export default class LiveSocket {
451458
}
452459

453460
owner(childEl, callback){
454-
let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), el => this.getViewByEl(el)) || this.main
455-
return view && callback ? callback(view) : view
461+
let view = maybe(childEl.closest(this.viewSelector()), el => this.getViewByEl(el))
462+
// If there's a rootViewSelector, don't default to `this.main`
463+
// since it's not guaranteed to belong to same liveSocket.
464+
// Maybe `this.embbededMode = boolean()` would be a more clear check?
465+
if (!view && !this.rootViewSelector){ view = this.main }
466+
if (view && callback){ callback(view) }
456467
}
457468

458469
withinOwners(childEl, callback){

guides/client/js-interop.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ except for the following LiveView specific options:
2424
* `uploaders` – a reference to a user-defined uploaders namespace, containing
2525
client callbacks for client-side direct-to-cloud uploads. See the
2626
[External uploads guide](external-uploads.md) for details.
27+
* `rootViewSelector` - the optional CSS selector to scope which root LiveViews to connect.
28+
Useful when running multiple liveSockets, each connected to a different application.
29+
See the [Connecting multiple livesockets](#connecting-multiple-livesockets)
30+
section below for details.
31+
32+
a CSS selector to scope which
2733

2834
## Debugging client events
2935

@@ -313,3 +319,42 @@ Hooks.Chart = {
313319
```
314320
315321
*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.
322+
323+
324+
### Connecting multiple liveSockets
325+
326+
LiveView allows connecting more than one `liveSocket`, each targeting different HTML nodes. This is useful to
327+
isolate the development cycle of a subset of the user interface. This means a different Phoenix application hosted
328+
in a different domain, can fully support an embedded LiveView. Think of it as Nested LiveViews, but instead of
329+
process-level isolation, it is a service-level isolation.
330+
331+
Annotate your root views with a unique HTML attribute or class:
332+
333+
```elixir
334+
# Main application serving a regular LiveView
335+
use GreatProductWeb.LiveView, container: {:div, "data-app": "root"}
336+
337+
# Cats application, which will serve the cats component
338+
use CatsWeb.LiveView, container: {:div, "data-app": "cats"}
339+
```
340+
341+
And initialise the liveSockets:
342+
343+
```javascript
344+
# Fetch the disconnected render
345+
let disconnectedCatsHTML = await fetch("https://cats.io/live", { credentials: 'include' })
346+
.then((response) => response.text())
347+
.catch((error) => console.error(error));
348+
349+
# Append it to HTML
350+
document.queryElementById("#cats-slot").innerHTML = disconnectedCatsHTML
351+
352+
353+
# Connect main liveSocket
354+
let liveSocket = new LiveSocket("https://root.io/live", Socket, {rootViewSelector: "[data-app='root']"})
355+
liveSocket.connect()
356+
357+
# Connect the cats liveSocket
358+
let liveSocketCats = new LiveSocket("https://cats.io/live", Socket, {rootViewSelector: "[data-app='cats']"})
359+
liveSocketCats.connect()
360+
```

0 commit comments

Comments
 (0)