Can I push an event to the server from a JS.dispatch() event handler?

Is there a way to push an event to the server from an event handler invoked via JS.dispatch()? pushEvent seems to be unavailable. Example code:

app.js

window.addEventListener("phx:custom_event", (event) => {
  // ...
  // some code
  // ...
  liveSocket.pushEvent(...) // doesn't work
  })

pushEvent is a method available to life cycle callbacks in client hooks via phx-hook. So if you define the event handler from within a client hook, you should have access to pushEvent.

// Example from the docs linked above

// HEEx template
<div id="infinite-scroll" phx-hook="InfiniteScroll" data-page={@page}>

// JS client hook
let Hooks = {}
Hooks.InfiniteScroll = {
  page() { return this.el.dataset.page },
  mounted(){
    this.pending = this.page()
    window.addEventListener("scroll", e => {
      if(this.pending == this.page() && scrollAt() > 90){
        this.pending = this.page() + 1
        this.pushEvent("load-more", {}) // note: not `liveSocket.pushEvent(...)`
      }
    })
  },
  updated(){ this.pending = this.page() }
}

let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, ... })

Right, I know I can use pushEvent from a hook, but I want to do it from a JS.dispatch() event handler. I also know I can use JS.push() as in <button phx-click={JS.dispatch(...) |> JS.push(...)}>, but that’s not what I need…

Hmm, not sure I see the problem, maybe a bit more context would help. ¯\_(ツ)_/¯

What makes JS.push not what you need? And why can’t you define the event handler for the event sent via JS.dispatch within a hook?

JS.push solution doesn’t work because the JS.dispatch handler does some reasonably complex processing, output of which I want to send to the server. So the ideal solution is to push the event from the handler.

As far as defining the handler in a hook, this would mean reworking a bunch of our code. We would need to add phx-hook attributes to elements, for example. We have multiple JS.dispatch handlers, and ideally we would just add a call to those. I am hoping for a simple way to accomplish this. I can push events from a phx-click handler with JS.push and from a hook with pushEvent, but not from a JS.dispatch handler?

Hmm, only one element with phx-hook would be necessary. That hook would define a mounted lifecycle callback that sets up all the window.addEventListener aka JS.dispatch event handlers that need to push an event to the server.

Not ideal and a bit roundabout, but if consolidating all the JS.dispatch event handlers is not workable, those event handlers could instead emit a relay event with the processed output as a payload to a generic relayer/hidden element as discussed here: DeadView => LiveView interaction?.

And skimming through the live_socket.js, the closest public method available would be liveSocket.execJS. It’s supposed to be used in conjunction with data attributes that embeds JS commands, but it may be possible to use directly.

If you desire, you can also integrate this functionality with Phoenix’ JS commands, executing JS commands for the given element whenever highlight is triggered. First, update the element to embed the JS command into a data attribute:

<div id={"item-#{item.id}"} class="item" data-highlight={JS.transition("highlight")}>
  <%= item.title %>
</div>

Now, in the event listener, use LiveSocket.execJS to trigger all JS commands in the new attribute:

let liveSocket = new LiveSocket(...)
window.addEventListener(`phx:highlight`, (e) => {
  document.querySelectorAll(`[data-highlight]`).forEach(el => {
    if(el.id == e.detail.id){
      liveSocket.execJS(el, el.getAttribute("data-highlight"))
    }
  })
})

source: JavaScript interoperability — Phoenix LiveView v0.20.2