DeadView => LiveView interaction?

I have a LiveView section embedded inside a generally DeadView based layout. I would like to interact with what LiveView does, using controls placed in the layout. I can imagine for example some JS, propagating clicks from DeadView control elements to hidden equivalents in LiveView section (didn’t try it out yet but it should work, shouldn’t it?) still - maybe there’s a more elegant way to achieve that?

Hmm, that’s an interesting scenario!

If the LiveView equivalent control elements are hidden and unused anyway, why not just set up a generic relay through one hidden element? The relayed event could be “unpacked” in the JS Hook and/or the LiveView handle_event callback.

  1. Click some control element in DeadView
  2. Dispatch resulting DeadView events to a “relayer” element in LiveView
  3. Relay event from LiveView element to socket with a client side JS hook
# JS for DeadView
const zombieButtons = document.querySelectorAll(".zombie-button")
zombieButtons.forEach(function(button} {
  button.addEventListener("click", function (event) {
    this.dispatchEvent(new CustomEvent("RelayToLiveView", event))
  })
})
# HEEx template
<input type="hidden" id="event-relayer" phx-hook="EventRelayer" />
# Client Side JS Hooks for LiveView
let Hooks = {}
Hooks.EventRelayer = {
  mounted() {
    this.el.addEventListener("RelayToLiveView", event => { 
      this.pushEvent("relay", event)
    })
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
...
# LiveView
def handle_event("relay", event, socket), do: ...
2 Likes

Yes, something like that. And your suggestion is already better. Especially with larger number of controls and possible actions. :+1:

What I would still like to know (not necessarily use though) is whether there is a way to interact with LiveView socket/session from outside of it. Something like the above but server-side. Le’t say the DeadView control does a POST to the server. Receiving code parses the POST-received message and updates the socket’s assign somehow. I guess the only way would be through PubSub? Or is there anything more “direct”?

The liveview is just a normal process, so you can send to it. You’d need to encode the LV PID somehow into the POST message, possibly with :erlang.pid_to_list |> :erlang.list_to_pid.

Then you just need a handle_info function in the liveview.

https://www.erlang.org/doc/man/erlang.html#pid_to_list-1

list here is an erlang charlist, but effectively a string you can encode into a field. Never used this myself, unsure if there are pitfalls. You’ll need to convert the elixir binary/string into a charlist on the way back to erlangland. :erlang.pid_to_list(self()) |> to_string() |> to_charlist() |> :erlang.list_to_pid().

Pardon the ignorance, but how do I get the right one in the first place? I mean the LiveView PID from outside of the process itself? Is it reliably accessible anywhere?

Not sure I’d recommend this for production, but you could use the Registry to track the LiveView pid when setting up them up in the mount lifecycle callback.

It all does seem very roundabout though. Since a socket’s been already set up, it may make sense to use the quicker stateful websocket transport considering the overhead of sending back messages through the stateless HTTP transport. And I’d imagine the added speed boost is very relevant for handling media control interactions. ¯\_(ツ)_/¯

No not ignorance, a poor omission on my part. Encoding the PID in the form is an embarrassingly terrible suggestion anyway :flushed:. Registry is built for this, looking up processes by a (custom) key and it automatically handles removing dead processes, etc. I am not sure when I would really preference this here though. With 1 → 1 it seems less complicated to dispatch in dom, with N → M your probably better off with PubSub as its already setup.

As for the original question, I would do as codeandpeace says with a redispatch hook.

In

should be:

window.addEventListener("RelayToLiveView" […]

shouldn’t it?

How would you suggest encoding the action? event.target.id? Or any better ideas than that?

No problem. I was wondering whether I missed something obvious. Also I myself put a disclaimer that I am not necessarily going to use this approach. It’s more of exploring the landscape of options. I simply like to take educated decisions :wink:

This is how I would do it:

const ProxyDispatch = {
  mounted() {
    // use namespaced event on element so we can support N embedded views
    this.el.addEventListener("my-app:proxy", ({ detail }) => {
      const { dispatcher } = detail
      dispatcher.dataset.js &&
        this.liveSocket.execJS(this.el, dispatcher.dataset.js)
    })
  }
}
<%!-- You could wrap in your own function that returned [{:"phx-click", ...}, {:"data-js", ...}]
      and I think heex would splat it correctly for you. --%>
<button
  phx-click={JS.dispatch("my-app:proxy", to: "#dispatch-target [phx-hook=ProxyDispatch]")}
  data-js={JS.push("wake_up", value: %{some: "data"})}
>
  Proxy event
</button>

Though that sort of assumes your dead view isn’t dynamically changing the push-event data after render. This lets you keep the interaction element and the interaction event proximal and everything is pretty “phoenixy”.

You need the separate data-js attribute because you can’t embed the JS ops inside another currently.

Ideally you could just call phx-click={JS.push("my_event", target: "live-view-selector")} but you can’t currently do that from dead views (for now?).

1 Like

Yup, good shout out – leftover copy pasta!

@soup Ahh that’s neat, I assumed there wouldn’t be JS commands support within deadview templates but your post got me to investigate and it turns out that it was added in LiveView v0.18.0!

I wouldn’t bother with ids and just encode the action directly into the event. For example, if you go with the JS.dispatch, it can take an optional :detail map.

<button phx-click={JS.dispatch("RelayToLiveView", detail: %{event: "play"})}>Play</button>

You could also specify a to: "#event-relayer" as well, but it probably isn’t necessary if the phx-hook sets the event listener at the window scope.

:smiley: Hehe… I kept scratching my head for some time and even started to write a reply, trying to remind @soup gently that the button is in DeadView. But before finishing it I said to myself “heck… let’s try” and… it apparently works! Although I had to use Phoenix.LiveView.JS (idk where I should alias it in order to use simply JS)

And yes, this simplifies things a whole lot

1 Like

Nice! And yeah, it really does simplify things quite nicely. Once you’re happy with and settled on a solution, please do report back! :smiley:

I think previously it needed to be done manually e.g. JS not working in Live Component without alias, but not in documentation · Issue #1688 · phoenixframework/phoenix_live_view. That said, for newly generated Phoenix apps as of v1.7, use HelloWeb, :html sets up that alias among other things.

# in `lib/hello_web.ex`
def module HelloWeb do
  ...
  def html do
    quote do
      ...
      # Include general helpers for rendering HTML
      unquote(html_helpers())
    end
  end

  defp html_helpers do
    quote do
      ...
      # Shortcut for generating JS commands
      alias Phoenix.LiveView.JS
      ...
    end
  end
end

source: phoenix/installer/templates/phx_single/lib/app_name_web.ex at f8c0ad26ee031ee87a6e7307f6dc1f7722134dd5 · phoenixframework/phoenix · GitHub

Not so fully happy yet. So far I have

/* app.js */
Hooks.ZombieRelay = {
	mounted()
	{
		window.addEventListener("phxapp:zombie-click", (e) => {
			console.log(e);
			e.target.click(e.detail);
		});
	}
}
<!-- DeadView -->
<button class="btn-secondary" phx-click={Phoenix.LiveView.JS.dispatch("phxapp:zombie-click", to: "#zombie-relay")} >Button Content</button>

and

<!-- LiveView -->
<input type="hidden" id="zombie-relay" phx-click="zombie-call" phx-hook="ZombieRelay" />

This works nicely. I am still stuck on passing additional data though. I don’t really need it yet but would like to have it there already. Apparently need to spend a bit more time on this

Nice, what’s the reasoning behind simulating a click on the LiveView hidden input rather than using JS.push_event in the relay hook?

Regarding passing addtional data, JS.dispatch accepts an optional :detail map that could come in handy.

Well, that’s the initial idea I had being implemented and so far the only one that I got working as expected. Must be missing something obvious.js

So far limited success with this one too. Reason most probably the same as above :wink: OTOH I admit I didn’t spend too much time on it either

Update – now I start to like it :wink:

/* app.js */
Hooks.ZombieRelay = {
	mounted() {
		this.el.addEventListener("phxapp:zombie-click", (e) => {
			this.pushEvent("zombie-call", {action: e.detail.dispatcher.dataset.action});
		});
	}
}
<!-- DeadView -->
<button phx-click={Phoenix.LiveView.JS.dispatch("phxapp:zombie-click", to: "#zombie-relay")} data-action="example-action">Button content</button>
<!-- LiveView -->
<input type="hidden" id="zombie-relay" phx-hook="ZombieRelay" />
2 Likes

Very nice!

And it looks like it’d be very easy to send multiple parameters via data attributes with this.pushEvent("zombie-call", event.detail.dispatcher.dataset).

Also, I still can’t get over how apt and funny of a descriptor zombie is for dead to live functionality! :wink:

1 Like

Yes, I extract only the “action” here, but generally yes - multiple data points can be trivially passed over this way. And – BTW – it would also work the way you wrote it above because relevant data points are then extracted server-side on handle_event() but if there were other, irrelevant for the action data- attributes attached, it would keep sending unneeded baggage down the wire on every interaction

Guess whom to thank for this funny idea, which I couldn’t resist to… well… reuse :wink:

All in all it turned out nicely in only a handful of lines. The crucial part was to realise that Phoenix.LiveView.JS can in fact be used in DeadView templates. Thank you @codeanpeace and @soup !

1 Like

Here’s a very dumb question: what is a DEADVIEW

Thanks