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

support multiple livesockets #3564

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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(){
return `${PHX_VIEW_SELECTOR}${this.rootViewSelector || ""}`
Copy link
Member

Choose a reason for hiding this comment

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

Should this be inversed, so people could do #foo-baras the root view selector if they want to? In such cases, should they end it with a space?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking it would simplify things if we just ignore the PHX_VIEW_SELECTOR and only use the user-defined selector?

The initial idea behind composing both together was because I didn't notice we could customise the container of the LiveView. So my initial implementation had something like: :has(.some-class-inside-the-lv-template).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any reason not to just replace PHX_VIEW_SELECTOR completely with a user defined selector?

new LiveSocket("/live", Socket, {params: {viewSelector: ".my-selector"}})

We can document users need to customise the LiveView :container in that case. It can be done globally in use MyAppWeb, :liveview or easily extend to accept as param use MyAppWeb.LiveView, :liveview, app: :something 🤔

Copy link
Member

@josevalim josevalim Dec 10, 2024

Choose a reason for hiding this comment

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

I think that would be my vote but we need to tell people they will want to check for [data-phx-session] unless their selector points directly to the LiveView container.

}

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 }
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't document this as prominently. I think the documentation for the liveSocket option is enough.


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' })
Copy link
Collaborator

Choose a reason for hiding this comment

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

would that be a controller doing a live_render, or do you actually have a live "/live" route in your router?

.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()
```
Loading