Is it possible to focus on specific element in form after submitting, without a custom hook?

I’ve been using the following hack:

def push_js(%Socket{} = socket, %JS{} = js) do
  push_event(socket, "js", %{id: socket.id, js: Jason.encode!(js.ops)})
end

# Usage in `handle_event/3`:

socket
|> push_js(JS.focus(to: "#element"))

In app.js:

window.addEventListener("phx:js", (e) => {
  let el = document.getElementById(e.detail.id)
  liveSocket.execJS(el, e.detail.js)
})

I say “hack” mainly because this accesses JS.ops, but JS is an @opaque struct (meaning we can’t rely on its structure).

In any case, it comes in handy whenever you want to execute some simple JS only after the server has acknowledged an event. For example, this does not work as one might expect: phx-click={JS.push("remove") |> JS.hide()}. (Unless I’m doing something wrong, which I probably am! :slight_smile:) Instead, phx-click="remove" and push_js(socket, JS.hide(to: "#el")).

It’s also useful for pushing simple unsolicited JS commands from the server to the client in reaction to e.g. PubSub events.

Of course, you can just manually push_event and write custom JavaScript every time you want to emulate JS commands, but that’s no fun. Especially when all you’re doing is something simple like focusing a field or hiding an element. That is, operations already supported by JS.

I like the phx:focus solution as it’s fairly reusable, but it could quickly get unwieldy as soon as you want to emulate other JS operations, e.g. JS.focus_first()phx:focus_first, JS.hide()phx:hide, etc. And you have to remember each bespoke event, whether to pass a DOM id/query selector, the various options, and so on. Plus you lose out on useful JS functionality like transitions (or have to re-implement it - poorly).

While writing this, I actually happened upon another post by @DecoyLexi who comes to basically the same solution (but to solve a slightly different problem): Trigger CSS transitions from server side - #4 by DecoyLexi

So I wonder if a PR might be in order? Or is this a gross misuse of LiveView? :sweat_smile: