8

I have an input.onkeydown handler and I check input.value after setTimeout(..0).

I expect input.value to have the new value when the setTimeout callback runs.

In all browsers except Firefox it does. In Firefox it's not always the case.

The code to check is:

<input id="input">
<script>
  input.onkeydown = function() {
    setTimeout(() => this.value = this.value.toUpperCase());
  };
</script>

The demo: http://plnkr.co/edit/rZmiHdttSXNdpKkR8YbH?p=preview

As I uppercase the input value after setTimeout(..0), it should be uppercased always. But as said, in Firefox it's not.

Here's the demo video; the first few seconds demonstrate the problem: https://jmp.sh/9XSROQ2

The relevant spec part is https://dom.spec.whatwg.org/#concept-event-dispatch.

Am I not getting something, or is this a long-standing bug in Firefox?

P.S. If I add console.log in setTimeout, I sometimes see the old value.

P.P.S. The purpose of this question is to know if I understand setTimeout correctly or not. I'm familiar with a variety of ways to uppercase input; please do not suggest oninput, requestAnimationFrame, or such.

Boann
  • 48,794
  • 16
  • 117
  • 146
Ilya Kantor
  • 581
  • 5
  • 11
  • 2
    `setTimeout` gets throttled. It won't run after 0ms, but rather after 5ms. What are you trying to achieve with `setTimeout` ? – Jonas Wilms Jun 25 '19 at 08:48
  • Sorry, but you are mistaken. It's throttled after several nested calls, per the spec. – Ilya Kantor Jun 25 '19 at 08:52
  • 2
    The input changes on `keypress`, not on `keydown`. – Bergi Jun 25 '19 at 08:53
  • Indeed, throttled was the wrong word (cause you are right, throttling in the sense that the timer time gets increased only happens after a few recursive calls). However you add multiple "concurrent" tasks into the task queue shortly after another (the events get added, the timer gets added) so sometimes their order might be different. – Jonas Wilms Jun 25 '19 at 09:00
  • It works as expected for me on FF 56 - the input gets uppercased. – CertainPerformance Jun 25 '19 at 09:32
  • 1
    Please see the demo. Reproduces sometimes, not all the time! I just tried - 1st and 3rd letter did not get uppercased. And a colleague of mine observed such behavior also. – Ilya Kantor Jun 25 '19 at 09:48

2 Answers2

2

The actual specs are here and actually do not define the relation between the keydown and the change of the input's current value.

However they read, (emphasize mine)

For input elements without a defined input activation behavior, but to which these events apply, any time the user causes the element's value to change without an explicit commit action, the user agent must queue a task to fire an event named input at the input element, with the bubbles attribute initialized to true. The corresponding change event, if any, will be fired when the control loses focus.

So it is actually specced that the input event (that you should be listening to anyway) must fire asynchronously. Since this event is the one that testifies of a change in the current value, I don't think Firefox's behavior of applying the changes caused by the Key event in the next event loop is hardly a bug; remember that browsers have to make this change asynchronously after the event is dead (and can't be cancelled anymore by any handler).

Some additional notes (which may be related to a real cause btw), inputing a combined character (e.g ^ + a => â) I have a 100% repro in FF on macOs (because yes, I suspect it may also be related to the OS).

But of course, even if it doesn't go against any specs, and even if you have an easy fix (listen for input event) you may still want to file a bug-report at least for not behaving like other vendors.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    Nice answer. The input event will also cover cases where the keyboard is not used to input data (e.g. right click paste) so for the use case shown in the question it makes a lot more sense to use it – Bali Balo Jun 25 '19 at 14:17
  • For ``, is the explicit commit action releasing a key, so it fits? The problem is that the next event loop is not just next. It's planned "after the next". – Ilya Kantor Jun 26 '19 at 05:36
  • Got ~100% repro on Firefox Dev Win 10. – Ilya Kantor Jun 26 '19 at 05:48
  • @IlyaKantor Not sure to understand your question nor your remark... Could you please clarify? What do you call the "explicit commit action"? The HTML specs don't define anything like that. They just talk about handling changes made by the user, how these changes are made are not relevant to the specs. Your code is assuming a key event would do that, but there are many other ways to input text. – Kaiido Jun 26 '19 at 05:48
  • In this answer you cited the spec. The words "explicit commit action" are from your answer, your quotation from the spec =) So I asked how these words refer to the , why you assume that a text input has an explicit commit action (and hence your quotation is applicable). – Ilya Kantor Jun 27 '19 at 06:03
  • @IlyaKantor gosh you're right! I didn't copied the correct paragraph! Should have been the next one for `input[type="text"]`... Sorry. But actually doesn't change the core of the answer. – Kaiido Jun 27 '19 at 06:11
-1

I think its because Firefox is handling setTimeout(fn,0) synchronous also asked here: Why does Firefox handle setTimeout(fn, 0) synchronous but setTimeout(fn, 1) asynchronous?

Chrome and node replacing 0 with 1: Difference between setTimeout(fn, 0) and setTimeout(fn, 1)? So its always async for them. If you run it with setTimeout(fn,1), it also works in FF.

If you want to get it sync, use "oninput". The input value is not updated completely when the "onkeydown" event goes of. Working with, or without timeout in chrome/ff

input.oninput = function() {
    this.value = this.value.toUpperCase()
};
Cracker0dks
  • 2,422
  • 1
  • 24
  • 39
  • Chrome and node are not replacing 0 with 1. Maybe they did long ago? Try this in node, you'll see most executions happen in exactly the same ms. ``` function setTimeouts() { setTimeout(() => console.log(Date.now()), 0); } for (let i = 0; i < 10; i++) setTimeouts(); ``` – Ilya Kantor Jun 25 '19 at 09:28
  • maybe click the link and read this: "For Node.js, 0 is converted to 1, so they are exactly the same" and than this: "For Chrome, the result is quite similar with Node.js" – Cracker0dks Jun 25 '19 at 09:30
  • Also, about Firefox, so you suggest it's a bug or what? – Ilya Kantor Jun 25 '19 at 09:31
  • but 0 is not converted to 1. Not everything written in SO some time ago is true now, ok? – Ilya Kantor Jun 25 '19 at 09:31
  • "I think its because Firefox is handling setTimeout(fn,0) synchronous" definitely not synchonously. https://jsfiddle.net/nfu9d3st/ otherwise `after` would be logged before `0 ms`. The question is much more subtle when it comes to task queueing when interacting with external APIs (DOM in the linked question) – Yury Tarabanko Jun 25 '19 at 09:33
  • @IlyaKantor sure, but the code on the answer I refer to is still correct for chrome/node and ff. I get the exact same results. Don't know if 0 really is converted to 1 in the background or not, but the behavior suggests it. – Cracker0dks Jun 25 '19 at 09:45
  • @YuryTarabanko yep you are right, it is definitely not sync – Cracker0dks Jun 25 '19 at 09:46
  • About converting 0 to 1, I just gave the example that demonstrates that there's no conversion. Also you may want to see the up-to-date Node/V8 sources. Still, please let's return to the original question. Just reminding: the issue is Firefox-only! – Ilya Kantor Jun 25 '19 at 09:47
  • you can tell with your code that 0 is not converted to 1? You can set 0 to any value and the 10 logs would appear at the "same" given time... AFAIK – Cracker0dks Jun 25 '19 at 10:17