Phoenix LiveView callbacks - to put text input focus on the selected input field?

I have created a list with items inn LiveView which when an item is clicked is rerendered as an input field. Is there a callback or similar that I can hook into to also be able to put text input focus on the selected input field?

3 Likes

Add autofocus to the input tag

4 Likes

The autofocus attribute doesn’t seem sufficient in practice. The behaviour is not the same across all browsers. https://caniuse.com/#search=autofocus says it doesn’t work with iOS Safari.

I’ve updated the table editor demo with an autofocus attribute that should resemble @behe use case : https://liveview.cleverapps.io/tables

  • Chrome: works for the first input appearance, but not for the other subsequent inputs. The cursor is at the beginning of text.
  • Firefox: doesn’t work at all,
  • MacOS Safari : works for each input, the input is focused and all the text inside is selected,
  • iOS Safari: the input is focused, but the text is not selected and the keyboard isn’t shown.
2 Likes

This only seems to works once. The next input will unfortunately not receive focus.

1 Like

Changing an input type, or enabling, or adding it, then giving it focus is just one of those things that GUI libs worked out in the 1980s, but that web browsers still screw up royally. If you were to take the time to try, you’d find that you cannot do this reliably in straight browser-side Javascript. The closest you can come is to set a timer to fire to set focus after you’ve done the other stuff, and even then it’s iffy.

2 Likes

And this is the REAL reason why the Native vs. Web war never ends.

4 Likes

Similar question: I’d like to scroll to a newly added element. Basically https://github.com/chrismccord/phoenix_live_view_example/commit/1181c79955b96e66f2dcdb8357498f6cce8f61fb, but the new element may be out of view so I want to move it into the viewport.

Sounds like maybe there are no client side callbacks available? Any plans/thoughts on supporting something like that?

I have the same need, but decided to just let the item be a text input instead of click and turn it into a text input. If needed you can style the input so when it’s not focused it doesn’t look like an input, then when you focus it, the style changes and it looks like an input.

Found this nice workaround: https://elixirschool.com/blog/live-view-with-channels/

1 Like

This can now be solved with a phx-hook: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-js-interop-and-client-controlled-dom

1 Like

The following solution works for me.

Create AutoFocus hook in ./assets/js/app.js

let Hooks = {}
Hooks.AutoFocus = {
  mounted() {
    this.el.focus()
  }
}

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

Attach it to the first input field of a form

<.form let={f} for={@changeset} id="my-form"
  <%= label f, :name  %>
  <%= text_input f, :name, phx_hook: "AutoFocus" %>
  ...
</.form>

2 Likes

You can also focus an input by using a LiveView push_event if you need the server, or via the LiveView.JS push api without the server by adding JS.dispatch to it. So these are just slightly different flows with slightly different use-cases from the hooks example that @ibarch posted.

From the Server

Push a JS event from a handle_event or handle_input function. You catch your pushed event in either app.js with a window.addEventListener or in a LiveView hook, and then send your changes to the DOM from there.

So that’s something like:

  1. Start with an instigating event, for example a click.
<span phx-click="do_stuff_plus_focus_my_input" ...>Just click it</span> 
  1. Handle it and send a push_event. I’ve called mine focusElementById and sent a little map with the id of the input I want to focus. That map becomes the value for the JavaScript event object’s detail key, as you’ll see in the third step.
 def handle_event("do_stuff_plus_focus_my_input", %{"foo" => bar}, socket) do
    socket = assign(socket, foo: bar)
    {:noreply, push_event(socket, "focusElementById", %{id: "my-input"})}
 end
  1. Now catch that pushed event either from a hook or, as here, from app.js. The id we included (“my-input”) is in the event object as event.detail.id. Then just use standard JS’ .focus() and you’re done.
# app.js 
window.addEventListener(`phx:focusElementById`, (event) => {
  document.getElementById(event.detail.id).focus() 
})

Anyway the push_event docs go over all of this and are probably better written than what I have here.

Client-only Version

I’ve also finally noticed that LiveView.JS has a function called push that lets you compose events that you send from templates. We can use that with JS.dispatch to cut out the server.

  1. JS.push and JS.dispatch in action.
# plz excuse the formatting...
<span phx-click={ JS.push("choose_part_of_speech") 
                  |> JS.dispatch("phx:focusMe", to: "#my-input") 
                }>Click it good</span>

  1. We can grab that event with a window.addEventListener. This is almost the same as with the server, but we’re sending the id name as a string and not in a map, so our JavaScript event object in our listener now has our element ID under target and not detail in a map. So we access the id with event.target.id. Just small differences that keep me in the docs and that I hope will be automated away… You’ll see below.
# app.js
window.addEventListener(`phx:focusMe`, (event) => {
  document.getElementById(event.target.id).focus()
})

And that’s it. It’s all in the docs, but writing things out makes it easier for me to remember.

6 Likes

This post was so helpful that I decided to register on the forum and add to the thread.

In the Client-only Version, it seems like you don’t need the

JS.push(“choose_part_of_speech”)

that line actually [unnecessary] pushes the event to the server.

Simple

JS.dispatch(“phx:focusMe”, to: “#my-input”)

seems like is enough to do the trick. I didn’t try it in the code yet. This is based on the docs: JS.dispatch

1 Like

There’s also JS.focus now.

4 Likes

And does anyone know how to use it? I just added it like an attribute, but navigating between live_views did not set the focus. has this somehow to be activated?

For anyone else finding this, I have a case where when a user navigates to my live view I want a specific input field to have the focus immediately (i.e. a search form).
Using the lifecyle events and JS.focus, it was as easy as:

<.input field={@form["q"]} phx-mounted={JS.focus()} />
4 Likes

I spend many hours trying to solve “focus problem”. And I’ve found a solution, which works with all three browsers I tested. Is working all the time, not once.
I used phx-hook, update function in hooks.js, where I put: document.getElementById("input_user_response").focus({ focusVisible: true })

phx-hook was attached to div and I wanted to set focus on textarea input outside of that div.
And my solution is working great, I don’t know why exactly. I suspect my solution is working because in that div where phx-hook is attached, is liveview component which visibility change after user click some button. So hook is run in the right time.

Focus is complicated: :focus-visible - CSS: Cascading Style Sheets | MDN

1 Like