Experimenting with the default LiveView Dependency Search Page => Should be a better way to run Javascript Code

Hi everybody,

For those not yet aware, it’s now possible to bootstrap a Phoenix app with some liveview using the --live flag.
The default sample application seems great as a starter example.

I wanted to play around with this app and I’m still not yet convinced with the No JavaScript part.

For example, currently if I hit the search button without inputting anything yet, we are receiving a flash alert.

For example I modified this code a little so that I receive an info message and without actually running the search on the server.
Very simple modification to do in page_live.ex by adding a pattern matching with an empty query:

# live/page_live.ex
def handle_event("search", %{"q" => ""}, socket) do
  {
    :noreply,
    socket
    |> put_flash(:info, "You must provide something to search.")
  }
end

But I wanted to make the input field automatically focused.
I started by setting an id to the input field so that I can somehow target it and run the JS .focus() function on it.

# live/page_live.html.leex
<input id="search_input" type="text" name="q" value="<%= @query %>"
  placeholder="Live dependency search" list="results" autocomplete="off"/>

The following simple JS code would then do the job

document.getElementById("search_input").focus();

However I’m don’t know how to execute some bit of code within a liveview response.
But I saw that there is now some JS support using hooks.

The problem is that the hook must be set to an element we want to follow. Here since I outputting a message on the flash div, I tried to set a hook there:

# templates/layout/live.html.leex
<p class="alert alert-info" role="alert"
  phx-hook="FocusInput"
  phx-click="lv:clear-flash"
  phx-value-key="info"><%= live_flash(@flash, :info) %></p>

Then I added the following in the Javascript:

// assets/js/app.js
function focusInput() {
  document.getElementById("search_input").focus();
}

let Hooks = {}
Hooks.FocusInput = {
  updated() {
    focusInput();
  }
}
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks })

But it doesn’t worked… Before trying Hooks, I tried to run some JS using an event listener on the phx:update event…
And then I said myself that maybe here the hook on updated() is not when it’s finished but before…
So I tried to put some timeout like so:

// assets/js/app.js
function focusInput() {
  setTimeout(() => { document.getElementById("search_input").focus(); }, 10)
}

And it worked! A little as 10ms did the job.
While I’m not sure why a timeout is necessary I also noticed that it worked only the first time.

The reason is because once the Flash message is displayed, and since the message itself is the same, nothing new is displayed and there is no actual update.

So I added to the flash message a timestamp like:

  |> put_flash(:info, "You must provide something to search #{DateTime.now!("Etc/UTC")}")

And now everything is working.

While I’m convinced that LiveView has huge value. I’m not sure that it could ignore JS in the client side.
There are some kind of very UI oriented things (like focusing inputs, displaying error messages, scrolling the view etc.) that I’m not sure LiveView can do by itself (I mean without running JS code).

Does anyone know how I could do this input focusing task in a better way?

I mean if possible without the timeout and without having to deal with a timestamp.
I don’t try it, but using a dedicated hidden span to receive the timestamp might work…
I’m not sure how to handle HTML within Flash message so I tried to visibility: hidden; and even display: none; on the whole Flash div. And it’s still working because the underlying HTML is still changing.

The HTML :autofocus attribution make be enough in this case, but it varies by browser. For your hook, you should be the hook directly on the input, <input id="search" phx-hook="AutoFocus">

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

Hi Chris!
Pleasure to see you here!

The HTML :autofocus will do the job upon first display (and/or first/every mount I guess)…

However in the use case above since I don’t want to put the autofocus right away…
I could have done a bad job explaining the use case, so sorry for the confusion…

But I wanted to apply the autofocus upon a blank search… I know that an HTML required could do the job as well but, here the main goal for me was to learn how to run some bit of JS (here autofocusing on an input, but it could have be scrolling the page).

And I think that I should set the hook on an element that is actually impacted by a life-cycle event, like an updated

Since a flash message is displayed, I set it on the flash div

But I’m even thinking of a dummy display: none; element that can receive (being updated with) some data (literally some text string as inner HTML).

And in that Hook I can consume that data as some command and run arbitrary JS code, which will act like a switch case…

I’m not yet onto channels and live views, but I guess that for sending (and then executing) some JS code, channel might be more suitable…