Phoenix_live_view: Focused input field gets blurred on render

Created on 30 May 2020  路  16Comments  路  Source: phoenixframework/phoenix_live_view

Environment

  • Elixir version (elixir -v): 1.10.3
  • Phoenix version (mix deps): 1.15.3
  • Phoenix LiveView version (mix deps): 0.13.2
  • NodeJS version (node -v): 12.14.1
  • NPM version (npm -v): 6.13.4
  • Operating system: Linux Mint
  • Browsers you attempted to reproduce this bug on (the more the merrier): Firefox, Chrome
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes

Actual behavior

Issue #1
If an input field is focused while it's content is being changed either by another user (e.g. another browser or the system itself), the value of the field (the HTML) wouldn't change. If using two Firefox browsers (say, one in Linux and the other on another machine or in the vmbox on the same machine), the change will be applied to the state, but it will be reflected in the HTML of the browser applying the change it ONLY IF it involved blurring the field there first. while the other FF browser with the same field focused will not register any change. Even if there is just one browser changing the state under the focus without blurring the field, the value will not change.
On the other hand, if there's a Chrome and Firefox they will be competing over the field value and upon receiving the change, the other browser will send its own.

Issue #2
In a case in which the change is applied, via blur event (e.g. on tab/shift tab or a click), the focus will be lost altogether, that is the element that gains the focus prior to having the HTML re-rendered by LiveView will lose it after rendering is done. This is particularly problematic when focus is to move from one input field to another and is expected to stay where it was moved to.
Note: This issue was introduced sometime from 0.8.x as it wasn't there with 0.7.x

Issue #3
After the initial rendering (on mount), when focusing on any field input field and then moving the focus by tab/shfit tab or even click to another input field, it will lose it even if there wasn't any change. After clicking again on an input field, the tab/shift and clicks will work well and the focus will remain where expected (unless of course, the value is change in which case the Issue #2 will take place).

Expected behavior

1) When a system state is changed (and propagated to subscribed LiveView instances), the change should be rendered evenly under any focused input fields while the browsers should not be inclined to override this change by sending a competing event to LiveView because their own value in the focused field is different, but accept the received change instead.

2) When changing value of an input field by moving the focus elsewhere, the focus should stay where it was moved after LiveView rendering the change.

3) Similar to 2) but right after the page mount, the focus should stay where the user moved it to.

The repo

Please find the Chris McCord's Chirp app modified to use input fields (and the corresponding blur events) in the post components to clearly demonstrate the first two issues. The third issue will most probably be resolved when the second one is.
The following is for replicating the said issues

  • Get the repo from: https://github.com/DaTrader/chirp_modified
  • Go to localhost:4000/posts and/or 10.0.2.2:4000/posts in a vmbox browser
  • Enter a couple of posts unless you already have them
  • Click on the input field of the body of one of them on a browser on Windows in vmbox (the focus will remain visible there when you click back to Linux)
  • Click on the same input field in a Linux browser
  • Change the value (the value in the other browser with the focus on the same field will not change or if Chrome will fire it's own event and change the state back to where it was with the FF browser on Linux now having a stale state)

as for the Issue #2, all it takes is to click on any of the body input fields, change the value and press tab or shift tab. The focus will be gone (instead of remaining where the tab or shift tab moved it to)

All 16 comments

Issue 1: If an input field is focused while it's content is being changed either by another user (e.g. another browser or the system itself), the value of the field (the HTML) wouldn't change.

This is expected behavior if I understand well the issue:
"For any given input with focus, LiveView will never overwrite the input's current value, even if it deviates from the server's rendered updates." (from the docs)

As for the other 2 issues, the way you do the update looks problematic to me:

update(socket, :posts, fn posts -> [post | posts] end)
  • you defined posts as a temporary_assigns, so posts in update will always be empty ([])
  • basically what you do here is just: [post], but since this post isn't a new post (new id), it won't be prepended (the phx-update action you defined), but just updated; being re-rendered, the focus is indeed lost;

To have your use case working, that is not losing focus as you complete the form navigating with the tab button, consider using the LiveComponent as "source of truth".

# post_component.ex
def handle_event("update_body", %{"value" => value}, socket) do
+ {:ok, post} = Chirp.Timeline.update_post(socket.assigns.post, %{body: value})
+ {:noreply, assign(socket, post: post)}
- {:noreply, socket}
# timeline.ex
def update_post(%Post{} = post, attrs) do
  post
  |> Post.changeset(attrs)
  |> Repo.update()
- |> broadcast(:post_updated)
end

With these changes, I'm able to "tab" through the form completing all input fields without losing focus.

The way I understand the html focus thing, any kind of html input re-render will lose the focus, so in this case I believe it's better not to concern the parent LiveView with this and rather handle it at the LiveComponent level.

This is expected behavior if I understand well the issue:
"For any given input with focus, LiveView will never overwrite the input's current value, even if it deviates from the server's rendered updates." (from the docs)

You're right. It's in the docs (haven't noticed it before, my bad). Anyway, is there a way to make a request to make this optional i.e. to enable having own code gain a complete control of what gets changed? I am asking for as it is now it seems optimized for simple CRUD apps while rendering free-style apps using input fields outside of the "submit form" paradigm becomes difficult if not impossible because of this limitation.
Imagine you had a table (a grid layout) with input fields. And you wanted another user to see you editing it. Their focus on a field will not permit it. Or even simpler. Imagine this functionality in which you need to have a Delete key clear the content of a cell on when it is not being editing (like in excel). You can't do it as LV works now. The only option still available is to have divs for cells/fields that are not being edited and an input field for the one that is. But this requires otherwise unnecessary sizing alignment of these two different html element types. I don't get it why the choice was to unconditionally disable this feature and I am asking to make it optional.

With these changes, I'm able to "tab" through the form completing all input fields without losing focus.

I'll get into this tomorrow. My real UC does not even involve LV components (yet), but a table the old LV style and with LV 0.7.1. this focus issue was not there. It showed up when I upgraded to 0.13.2 and I only used Chris' app for the simplicity (it was relied on components, so I simply add to it). Anyway, it's not about components. It does not work properly without them either.

@sfusato
Ok, I now see what you're suggesting and I have a couple of problems with that.
First, the suggestion seems more like a workaround as it basically changes the process flow, for there is no longer a uniform (broadcast-based) notification to all subscribing liveviews evenly. Besides breaking the design pattern (which all of the parts of my real-life app tend to follow religiously and rightfully so), by avoiding the double messaging (i.e. removing the broadcast) you now need to tell apart among subscribers as you're treating the actor instantiating the UC differently, for as there is no broadcast now, other users won't be notified of any update.
Second, the mere notion that a workaround like this is required sheds light on where the true problem is.

Therefore, can you please instruct me of a procedure to post an official request to the Phoenix/LiveView team to add an option to liveview (e.g. a parameter at the component or push event level)? This option would have the LV behave (at per component level or event level) in such way to simply render everything regardless of what element is focused and when finished rendering simply refocus on the element (by its id) if it still exists or leave not refocus if it does not. So a process like this can be possible:
1) my hook code moves the focus where it pleases and in doing so triggers the blur event which my focusOutListener catches and pushes a change_value event to the LiveView
2) LiveView renders the diffs
3) LiveView returns the focus where my code from 1) moved it to, OR does not refocus at all, whichever is easier or less error-prone to implement
4) LiveView fires updated event
5) My JS hook listener catches the LiveView updated event and does with the focus as it sees fit
6) At this point LiveView does no longer do anything with focus or otherwise it risks undoing what my code did in 5) which is what most probably is happening now (post 0.7.1)

My suggestion is the option be called :focus_agnostic.

I emphasize again that apart from the not-rendering focused input content, the above used to work properly and without glitches up to 0.7.1. At the time there was a bug that required my code to postpone refocusing with (as suggested by Chris McCord) with setTimeout( move_focus, 0), but he fixed that in 0.8.x. Other than that particular bug the rest went smoothly and I had no problem with LiveView refocusing properly "just" not rendering the focused input element content.

Lastly, I would really like to see this officially implemented for a very simply reason (other than me having to fork the LV JS code). For the record, my app is a relatively big one and most of its UI is completely customized (read: no simple CRUD form stuff) and I've been very satisfied with the underlying requirements for the LiveView and how it was conceptualized except for the parts like this one, where the framework authors diverge from the original path to provide a generic alternative to substitute JS client-side rendering by adding certain use-case specific limitations such as "liveview will never render the content of a focused input field". Client-side JS rendering has no such limitations and I wholeheartedly suggest that LV does not either, or at least, that it offers an opt-out. Unfortunately at this moment I cannot yet share what we're building on LV (our app) but once we go in production with it, I believe the team will be proud of how LiveView was put to work. But for this to work properly and without side-effects and glitches (I now implement various workarounds that generate unnecessary blinking i.e. unfocus, change state, catch update, refocus) and make the otherwise single pass actions multi-pass all of which results in unnecessarily challenged look and feel.

Hi @DaTrader,

It is a bit unfair to compare Client-side JS forms with LiveView because Client-side JS forms are not receiving updates over the wire and therefore do not need to worry about latency. We have to block updates to focused inputs exactly because latent updates may revert what the user recently typed.

For example, imagine a form with two fields: an organization name and the organization slug. By default, we fill in the slug field as you are typing the organization name. Now, imagine the organization name is "Dashbit". If you are faster typer over a slow connection, you can finish writing the name and jump to the slug input while the slug is still "dashb". The server will eventually send the rest of the "inferred slug" but if we allowed updates, that can now overwrite what you are writing.

Implying that we have optimized for simple CRUD apps is also a bit discouraging, because it ignores the complexity of the problem, and the fact that even getting things like "focusing inputs" is non trivial, because browsers are inconsistent between them.

In any case, I believe you can always fully replace an input if you also replace its ID. If that's still not enough, we would love to read a concise description of the issue and with possible solutions.

Thank you!

@josevalim

Hi Jos茅,

I apologize if my reflection on the LV focus behavior side-effects have offended you. It wasn't meant to. I do appreciate all the effort invested in the LiveView project and besides certain flaws and bugs, I find it truly fantastic (wouldn't have taken the no-plan-B path of building my app on it a year ago if I didn't)

Back to the issues now. My problem with the focus related behavior is as you can tell from my bug report, two-fold. First it is about the issue that you have commented on while the second problem is that the behavior (or the side-effects thereof) have from _my UC viewpoint_ deteriorated after the v 0.7.1 (that I am still using for development for this particular reason), and the later versions (e.g. 0.13.x).

For the first problem, I simply suggest you add a flag which will have everything rendered, including the focused fields. This is absolutely mandatory for any multi-user editing of a same table (spread-sheet like) application. Refocusing for the other simultaneous users shouldn't be an issue as the data on the altered cell(s) can be passed via template so they can refocus accordingly, while changing a cell that somebody else is editing is something the app itself should not permit, and no longer see a problem here.
Please note that the problem in your example above is only present if you base your cell editing on the change event (and have such a feature of an auto-generated slug) while with a typical table-editing UC the changes are made on the blur event (at least I make them so) so this problem does not exist.
The solution is easy. You can simply have this option added as a phx- tag to the element (it will address your example easily).

As for the other issue (issues #2 and #3 in my initial post), I imagine all it takes is not to undo the focus movement done in the hooks code. Say you have a default post-rendering behavior which I imagine tends to refocus at the same field before the rendering started. First thing here, it is important that you get the very last focus, the one that may have possibly been set by a hook code before triggering the event that had the LiveView re-render. Second, post-rendering you need to have some feedback from the user on whether they want their hook to set the focus or it should be your default behavior. A hook flag or a callback function can fill this purpose.

Best,

Damir

Thanks for the positive reply. Are you saying a phx-update-when-focused attribute would solve your problem?

Yes. It would solve the first problem.

The other problem (issues #2 and #3) is also important to solve. For example, the LV JS code that invokes the updated hook function, could expect a return value with an attribute set to element to focus at or call an optional hook function receiving the LV suggested element to focus at and returning its own if different.

Hooks.MyHook = {
  mounted: ..
  updated: ..
  ..
  refocus: el => {
    ..
    return myEl;
  }
};

That would require us to track focus ourselves and it is very complex, unfortunately, because browsers behave inconsistently (some focus on page updates, others do not, etc).

To be clear, it is hard for us to do it in a consistent and generic way, without overriding browser behaviour. Those are decisions that LV should not take, but of course you can take them in the context of your app, but it is up to you to make those choices.

Of course. However, what is important to me (and anyone else who needs the control over focus) is not to have LV reset the focus _after_ my code in the in the hook's 'updated' function sets it post-rendering because that's what it seems is going on now (in version 0.13.2), but was not the case with 0.7.1.

Btw, you can use a hook today to implement phx-update-when-focused. Just send the value as data-value or similar and update it from the hook.

You mean change the dom from js? But then I need to tag all my cells as phx-update="ignore" in order not to mess up the LV rendering, right? And if I do that then I guess I need to render them myself all together. Or I am missing something?

Or.. the input text value can be changed in JS regardless of LV and the phx-update? But in that case, what do I do with the divs containing innerHTML for text (I use divs for the free-style text cells)?
To be honest, I really don't know to which extent we are supposed not to deal with the html and the page content from JS, so I chose not to touch it at all.
But, now seems like a good time I learn what kind of change is permitted.

Ok. Here's what I did. I now have LV literally replace the input text element with a div when the user is not editing and the other way around when they are. I do it so that the element id stays the same though (as I need it that way for my JS code and CSS). despite of changing the element itself and it works perfectly fine on v0.7.1. Now, do you consider this a hack (because of the same id) or I can rely on it from now on? From where I stand this is a perfect solution.

AFAIK it is fine to completely replace a tag but keeping the same ID.

Great!
Once I polish the stuff on this component I'll try to upgrade to what then will be the newest version of LV and see if I can deal with the issues #2 and #3 somehow myself and if not report the problems again.
Jos茅, thanks a lot for getting back to me and above all thanks for Elixir as a whole. It truly is a gem.

Was this page helpful?
0 / 5 - 0 ratings