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

As the title says, …

I have a UI that is kind of like a data table, where I type values into cells and hit enter, to create a new row. Once I do, I would like to shift input back to the first cell.

Is there a ways, with the JS module to get live view to behave as described?

I did it with a hook (though I’m not sure it’s the cleanest approach)

<.form
    phx-hook="FocusFirstAfterSubmit"
    phx-submit={JS.push("create_entry") |> JS.dispatch("submitted", detail: %{id: id})} ...>
const FocusFirstAfterSubmitHook = {
  mounted() {
    let timeout

    this.el.addEventListener('submitted', { detail: { id } }) => {
      // clearing existing timeout ensures we don't lose focus if we spam submit
      timeout && clearTimeout(timeout)
      // the event is received by all forms, not just one, so we must check id
      if (detail.id !== this.el.id) { return }
      // 200ms timeout allows the form to rerender after submit
      timeout = setTimeout(() => {
        document.querySelector<HTMLInputElement>(`#${this.el.id}_duration_hours`)?.select()
      }, 200)
    })
  }
} 

But I’m unable to find a way to do it via just the JS module

I tried both of these, with and without a target specified

phx-submit={JS.push("create_entry") |> JS.focus()}

phx-submit={JS.push("create_entry") |> JS.focus_first()}

but what seems to happen is, after the form submits, the focus briefly shifts to the first element, and then returns to the one I I’m, as in some step is invalidating it.

I’m probably missing something related to how the components are re-rendered, but I guess I don’t know enough about the internals yet? Any ideas?

Do note that my page has several sections of rows, where each section gets a “new row” form, so is it possible that creates a problem? The fact that the dispatched ‘submitted’ event is received by all such forms makes me thing it’s something like that.

Cheers!

1 Like

You can define a custom hook by following Triggering phx- form events with JavaScript. The custom hook:

  1. emulates the submit.
  2. focus on what you want.

Or, you can try the autofocus attribute of HTML- autofocus - HTML: HyperText Markup Language | MDN . If it works for you, then you don’t need a custom hook.

That works, of course. A custom hook is what I have, but i would expect it would be possible just with some combination of functions in the JS.module, and, possibly, as you say, autofocus

I feel like I’m missing some knowledge on how the dom is being updated, where the in the thing I see going on

  • i submit the form
  • focus shifts to first input (as I say it should)
  • focus shifts back to current input (i don’t want this)

the last part wouldn’t happen.

Maybe some element or a set of elements needs more appropriate ids?

<form> submission comes with default behavior. Maybe you can try to prevent that.

I kind of tried that to.

I tried things like

<.form pxh-submit={JS.push("create_entry") |> JS.focus_first()}

This doesn’t seem to work at all.

I also tried the other way around

<.form pxh-submit={JS.focus_first() |> JS.push("create_entry")}

In this case, I do end up focused on the first field briefly, but then it looks as if the form re-renders and the focus is back on the field I was in when I hit enter.

I feel like I’m missing some understanding of what triggers rerendering, so I’ll try to figure it out from reading through

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Engine.html#module-phoenix-liveview-rendered

2 Likes

My guess: LiveView has code that, once the page is patched, it focus on the current input. Otherwise patches from the server would always lose input focus. My guess is that’s the root issue. It makes sense that it doesn’t work on phx-submit, as you are focusing on push and not when the patch is received.

2 Likes

I had a similar situation (table, validates a row, stores if valid, adds new row and focuses where I want it to focus) and ended up using custom hook too. Is that a practical problem in your case?

I think you can use push_event/3 in your handler to push the focus event to a specific element. Something like:

def handle_event("submit", form_data, socket) do
    # handle form submit
    push_event(socket, "focus",  %{id: "input_id"})
end

And then add a simple window event listener in app.js

window.addEventListener("phx:focus", (event) => {
  document.getElementById(event.detail?.id)?.focus();
});
4 Likes

It makes sense. And there’s that hole section somewhere in the docs about always trusting the frontend on the value of the input (so if the backend sets a new value after handling the change event, for example, it will not actually be reflected on the FE.

I’m wondering if it would also make sense to have tooling in the JS module, or somewhere else, to sort of jump around this.

I guess my question is, is there tooling around this already and I’m just missing it.

This is a very concise way to do it, for sure, but I personally prefer a hook-based solution over a “global” event handler.

It’s not an actual blocking problem here at all. Hook works and it’s what I’m using. Global phx:focus handler combined with push_event would also work. It just felt like something that would have a more “direct” solution, so the intent of the thread is to potentially learn more about the framework.

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:

You can read the JS command from an attribute rather than sending it around.