[LLV] Figure out how to assign IDs to LLVs - #702
Conversation
b16730d to
0d8ea0d
Compare
0d8ea0d to
13e41db
Compare
mat-hek
left a comment
There was a problem hiding this comment.
I only looked at component.ex for now
| """ | ||
| end | ||
|
|
||
| @doc false |
There was a problem hiding this comment.
| @doc false |
| if mirror_exists?(view), | ||
| do: LocalLiveView.MirrorToken.sign(assigns.endpoint, view, assigns.id) | ||
| comp_assigns = | ||
| Map.drop(assigns, [:__changed__, :view, :flash, :id, :mirror_id, :mirror_token]) |
There was a problem hiding this comment.
Should we drop id? In the docs we say we forward it
There was a problem hiding this comment.
We were dropping it before, so I assumed the id is not needed in comp_assigns.
Also we forward it as a DOM element id.
There was a problem hiding this comment.
We're not dropping it on main: https://github.com/software-mansion/popcorn/blob/main/local-live-view/lib/server/component.ex#L68 Even if it's a DOM element id, it's not accessible in assigns, and we claim so here: https://github.com/software-mansion/popcorn/blob/fk/llv-id-generation/local-live-view/lib/server/component.ex#L24 . I guess we should pass it to assigns, as that's what live component does
| socket = | ||
| assign(socket, | ||
| view: view, | ||
| id: assigns.id, | ||
| mirror_token: mirror_token, | ||
| mirror_id: mirror_id | ||
| ) |
There was a problem hiding this comment.
we never assign other assigns, so @comp_assigns in render_markup are empty
| render_markup(assigns) | ||
| end | ||
|
|
||
| defmodule Live do |
There was a problem hiding this comment.
| defmodule Live do | |
| defmodule Mirrored do |
| default_id(view_or_id) | ||
| end | ||
|
|
||
| socket_id <> dom_id |
There was a problem hiding this comment.
maybe separate them?
| socket_id <> dom_id | |
| socket_id <> "-" <> dom_id |
mat-hek
left a comment
There was a problem hiding this comment.
Also, Claude told me that this broke burrito example. Haven't checked this, did you? BTW, it would be worth running all examples' CIs when LLV changes, as these are our main tests.
| const maybeSetupMirrorChannel = (el: HTMLElement) => this.maybeSetupMirrorChannel(el); | ||
| this.socket.hooks.LocalLiveView = { | ||
| mounted() { | ||
| maybeSetupMirrorChannel(this.el); |
There was a problem hiding this comment.
This calls connectMirror under the hood, which sends action to popcorn, but popcorn may be not ready yet (see 2 lines below). Maybe it should be a part of mountView?
There was a problem hiding this comment.
I moved it to if pop.ready scope.
There was a problem hiding this comment.
I don't think it will work - if popcorn is not ready in time, it never runs. mountView is different, because we run 'startup scan' once popcorn is ready: https://github.com/software-mansion/popcorn/blob/main/local-live-view/assets/local_live_view/index.ts#L390.
There was a problem hiding this comment.
So moved it to mountView function
|
|
||
| def mirror_id(socket_id, view_or_id) when is_binary(socket_id) and is_binary(view_or_id) do | ||
| dom_id = | ||
| if String.starts_with?(view_or_id, "llv-") do |
There was a problem hiding this comment.
Does this work when someone passes a custom id to LLV? We don't require the id to start with llv-
There was a problem hiding this comment.
Yes - I removed that part.
|
|
||
| if mirror_exists?(view) do | ||
| ~H""" | ||
| <.live_component module={__MODULE__.Live} {assigns} /> |
There was a problem hiding this comment.
This will break when someone tries to render mirrored LLV outside of a LiveView, as live components cannot be rendered in regular views AFAIK
There was a problem hiding this comment.
I am aware of that - I planned to leave it as a known limitation.
There was a problem hiding this comment.
If so, we should raise a clear error in that case. Would the limitation apply only to mirrored live views?
| if (pop.ready) mountView(this.el); | ||
| }, | ||
| updated() { | ||
| maybeSetupMirrorChannel(this.el); |
There was a problem hiding this comment.
Why do we setup the channel here? Isn't it enough to setup it on mount? If so, can we remove the connect_mirror event and just put the id in pop.create?
There was a problem hiding this comment.
I removed the connectMirror event and move the mirror connection functionality to mount.
There was a problem hiding this comment.
So the call to maybeSetupMirrorChannel here is redundant, isn't it?
| # Default ID derived from view name: | ||
| mirror_id = LocalLiveView.Component.mirror_id(socket, "Cart") |
There was a problem hiding this comment.
This no longer works, does it?
| mirror_id = LocalLiveView.Component.mirror_id(socket, "Cart") | ||
| """ | ||
| @spec mirror_id(%Phoenix.LiveView.Socket{id: String.t()}, String.t()) :: String.t() | ||
| def mirror_id(%Phoenix.LiveView.Socket{id: socket_id}, view_or_id) do |
There was a problem hiding this comment.
as per above
| def mirror_id(%Phoenix.LiveView.Socket{id: socket_id}, view_or_id) do | |
| def mirror_id(%Phoenix.LiveView.Socket{id: socket_id}, id) do |
| # Default ID derived from view name: | ||
| mirror_id = LocalLiveView.Component.mirror_id(socket, "Cart") | ||
| """ | ||
| @spec mirror_id(%Phoenix.LiveView.Socket{id: String.t()}, String.t()) :: String.t() |
There was a problem hiding this comment.
| @spec mirror_id(%Phoenix.LiveView.Socket{id: String.t()}, String.t()) :: String.t() | |
| @spec mirror_id(Phoenix.LiveView.Socket.t(), String.t()) :: String.t() |
| if Phoenix.LiveView.connected?(socket) do | ||
| endpoint = socket.endpoint || LLVComponent.resolve_default_endpoint() | ||
| LocalLiveView.MirrorToken.sign(endpoint, view, mirror_id) | ||
| else | ||
| nil | ||
| end |
There was a problem hiding this comment.
I don't see a reason to sign the ID on each update, as it never changes. I think we can do
| if Phoenix.LiveView.connected?(socket) do | |
| endpoint = socket.endpoint || LLVComponent.resolve_default_endpoint() | |
| LocalLiveView.MirrorToken.sign(endpoint, view, mirror_id) | |
| else | |
| nil | |
| end | |
| cond do | |
| token = socket.assigns[:mirror_token] -> | |
| token | |
| Phoenix.LiveView.connected?(socket) -> | |
| endpoint = socket.endpoint || LLVComponent.resolve_default_endpoint() | |
| LocalLiveView.MirrorToken.sign(endpoint, view, mirror_id) | |
| true -> | |
| nil | |
| end |
There was a problem hiding this comment.
Or even better generate the ID only if it's not generated yet
mat-hek
left a comment
There was a problem hiding this comment.
Looks good, minor tweaks left. Note that there were quite some bugs, not covered by the tests. For now I'd at least run examples' CIs when LLV code changes.
| @@ -322,14 +318,14 @@ export class LLVEngine { | |||
| private maybeSetupMirrorChannel(el: HTMLElement): void { | |||
There was a problem hiding this comment.
nit: this could accept llvId, popView and popMirrorToken, so that the extraction stays in mountView
| if (pop.ready) mountView(this.el); | ||
| }, | ||
| updated() { | ||
| maybeSetupMirrorChannel(this.el); |
There was a problem hiding this comment.
So the call to maybeSetupMirrorChannel here is redundant, isn't it?
| within a parent LiveView process. | ||
|
|
||
| Accepts the parent `socket` (or directly its `socket.id`), and either: | ||
| * a view module name / string (calculates the default DOM ID) |
| if (!mirrorId || (mirrorId && this.channels[mirrorId])) return; | ||
| if (!el.dataset.popMirrorToken) return; |
There was a problem hiding this comment.
nit
| if (!mirrorId || (mirrorId && this.channels[mirrorId])) return; | |
| if (!el.dataset.popMirrorToken) return; | |
| if (!mirrorId || this.channels[mirrorId] || !el.dataset.popMirrorToken) return; |
|
|
||
| if mirror_exists?(view) do | ||
| ~H""" | ||
| <.live_component module={__MODULE__.Mirrored} {assigns} /> |
There was a problem hiding this comment.
We still don't throw a readable error when not in LV
There was a problem hiding this comment.
I am inable to catch this exception on this level because the error here will be raised by the page controller mechanism inside phoenix.
Right now it is failing like this:
1) test GET / renders the LocalLiveView mount points (LocalThermostatWeb.PageControllerTest)
test/local_thermostat_web/controllers/page_controller_test.exs:4
** (ArgumentError) cannot convert component LocalLiveView.Component.Mirrored with id "llv-ClockLive" to HTML.
A component must always be returned directly as part of a LiveView template.
For example, this is not allowed:
<%= content_tag :div do %>
<.live_component module={SomeComponent} id="myid" />
<% end %>
That's because the component is inside `content_tag`. However, this works:
<div>
<.live_component module={SomeComponent} id="myid" />
</div>
Components are also allowed inside Elixir's special forms, such as
`if`, `for`, `case`, and friends.
<%= for item <- items do %>
<.live_component module={SomeComponent} id={item} />
<% end %>
However, using other module functions such as `Enum`, will not work:
<%= Enum.map(items, fn item -> %>
<.live_component module={SomeComponent} id={item} />
<% end %>
code: conn = get(conn, ~p"/")
stacktrace:
(phoenix_live_view 1.1.28) lib/phoenix_live_view/engine.ex:20: Phoenix.HTML.Safe.Phoenix.LiveView.Component.to_iodata/1
(phoenix_live_view 1.1.28) lib/phoenix_live_view/engine.ex:146: Phoenix.HTML.Safe.Phoenix.LiveView.Rendered.recur_iodata/3
(phoenix 1.8.5) lib/phoenix/controller.ex:1015: anonymous fn/5 in Phoenix.Controller.template_render_to_iodata/4
(telemetry 1.4.1) /Users/franek/Desktop/Repositories/popcorn/examples/local-lv-thermostat/deps/telemetry/src/telemetry.erl:359: :telemetry.span/3
(phoenix 1.8.5) lib/phoenix/controller.ex:981: Phoenix.Controller.render_and_send/4
(local_thermostat 0.1.0) lib/local_thermostat_web/controllers/page_controller.ex:1: LocalThermostatWeb.PageController.action/2
(local_thermostat 0.1.0) lib/local_thermostat_web/controllers/page_controller.ex:1: LocalThermostatWeb.PageController.phoenix_controller_pipeline/2
(phoenix 1.8.5) lib/phoenix/router.ex:416: Phoenix.Router.__call__/5
(local_thermostat 0.1.0) lib/local_thermostat_web/endpoint.ex:1: LocalThermostatWeb.Endpoint.plug_builder_call/2
(local_thermostat 0.1.0) lib/local_thermostat_web/endpoint.ex:1: LocalThermostatWeb.Endpoint.call/2
(phoenix 1.8.5) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5
test/local_thermostat_web/controllers/page_controller_test.exs:5: (test)
| case connected?(socket) do | ||
| true -> | ||
| mirror_id = | ||
| LocalLiveView.Component.mirror_id(llv_rendering_socket_id, "llv-BurritoLive") | ||
|
|
||
| Phoenix.PubSub.subscribe(Burrito.PubSub, "llv_mirror:BurritoLive:#{mirror_id}") | ||
| LocalLiveView.Channel.get_mirror_assigns(mirror_id) | ||
|
|
||
| false -> | ||
| %{} | ||
| end |
| const Socket = this.socketClass(); | ||
| const csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); | ||
|
|
||
| const llvSocket = new Socket("/llv_socket", { | ||
| params: { _csrf_token: csrfToken }, | ||
| }); | ||
| llvSocket.connect(); | ||
| this.llvMirrorSocket = llvSocket; |
There was a problem hiding this comment.
We now always start the socket, no matter if there are mirrors or not. I think we can lazily start it in maybeSetupMirrorChannel
| if (typeof mirrorId === "string") { | ||
| this.channels[mirrorId] = channel; | ||
| } |
There was a problem hiding this comment.
seems always true
| if (typeof mirrorId === "string") { | |
| this.channels[mirrorId] = channel; | |
| } | |
| this.channels[mirrorId] = channel; |
Closes: #679
Summary:
LLV ID handling: Retained the default behavior. If a single instance of a LocalLiveView is present,
iddefaults to"llv-LLVModuleName". If multiple instances of the same LocalLiveView exist, the user must explicitly provide a uniqueidfor each instance.Mirror identification: The server uses
mirror_idto uniquely identify mirrors connected to a specific LLV instance. Themirror_idis a combination ofsocket.idand the component'sid.