I have a form with a formdata
event handler that updates a field of the FormData
if a certain state is found in the DOM. After the form submission, the DOM needs to be changed, but the formdata
event handler should still observe the old state. Currently, I achieve this by using setTimeout(() => {/*...*/}, 0)
in a submit
event handler on the same form, because this will enqueue the function to run later - hopefully, after the formdata
event is handled. My question is: Is this behavior guaranteed, and if not, is there a specification-backed way to accomplish this behavior?
In the specification of event loops, the first step for choosing what work to do next is described as:
Let taskQueue be one of the event loop's task queues, chosen in an implementation-defined manner [...]
The oldest task from this chosen queue is then run later on. This would mean, that if functions scheduled with setTimeout
are in the same task queue as the event handlers and if the formdata
event handler is scheduled before the submit handler is actually run, I would definitely be safe - I cannot find any evidence for that though. From the documentation of the formdata
event ("fires after the entry list representing the form's data is constructed") and the fact, that the formdata
handler is run after the submit
handler, I would even assume the contrary to be true - but that is not what I observed with the approach described above.
Your understanding is quite correct, and you are right that setTimeout(fn, 0)
may not always fire after a "related" event: it is indeed very possible that these events are fired from two different tasks, very unlikely they'll use the timer task sources, and you correctly identified the bit that would make the event loop "possibly" select a task from an other task source.
However in this exact case, you are safe.
The submit
event and the formdata
one are fired from the same "task"*.
If you look at the form submit algorithm, you can see that the submit event is directly fired at step 6.5, instead of being wrapped in a task that would get queued like it's often the case.
Let shouldContinue be the result of firing an event named
submit
at form [...]
Then in the same algorithm, without any in parallel or anything implying asynchronicity, we have the step 8 that says
Let entry list be the result of constructing the entry list with form, submitter, and encoding.
And in this constructing the entry list algorithm, at the step 7, we have the call to
Fire an event named
formdata
at form [...]
once again without any asynchronicity allowed.
So we can be sure that these events will fire without anything else in between (apart microtasks), and that your timer callback from the submit
event will fire after the formdata
callback, even two requestAnimationFrame
callbacks scheduled in the same frame (that are also "synchronous" for the event loop) won't be able to interleave there:
document.forms[0].addEventListener("submit", e => {
console.log("submit");
});
document.forms[0].addEventListener("formdata", e => {
console.log("formdata");
});
requestAnimationFrame(() => {
console.log("rAF 1");
document.forms[0].querySelector("button").click();
});
requestAnimationFrame(() => {
console.log("rAF 2");
});
<form target="target">
<input value="foo" name="bar">
<button>
submit
</button>
</form>
<iframe name="target"></iframe>
*Technically it does not even need to be from a task per se, you can very well force these events to fire from a microtask or a resize event callback etc.)