简体   繁体   中英

Prevent default action with a programatically dispatched event

In this talk at 32:50 the speaker shows this code:

const nextClick = new Promise(resolve => {
    link.addEventListener('click', resolve, { once: true });
});

nextClick.then(event => {
    event.preventDefault();
    // Handle event
});

He explains why it prevents the click event's default action when it's caused directly by user interaction, but not when the dispatch is programmatic, eg link.click() . Is there a simple way to make it work in the latter case the same as in the former?

Edit to elaborate on motivation

@Kaiido asked in the comments why I (or rather the speaker) use a Promise here – I assume he'd like to suggest attaching directly as an event listener the function which calls preventDefault . The promise is convenient if I want to combine it with other promises using Promise.all or some other combinator.

The video explains perfectly what happens, but it seems we have to paraphrase it anyway...

In the case of a "native" event, the browser will "queue a task" to fire an event , which will get dispatched to your target and invoke all the listeners one after another, and finally call their JS callback . Since the JS stack is empty between each callback execution, a microtask-checkpoint is performed , and the Promises that got resolved during these callbacks get their callbacks executed.
Only after all these listeners have been invoked, the default action will get executed, if the event's cancelled flag hasn't been raised.

 const link = document.querySelector("a"); link.addEventListener("click", (evt) => { console.log("event 1:", evt.defaultPrevented); Promise.resolve().then( () => { console.log("microtask 1:", evt.defaultPrevented); // false evt.preventDefault(); }); }); link.addEventListener("click", (evt) => { console.log("event 2:", evt.defaultPrevented); // true Promise.resolve().then( () => { console.log("microtask 2:", evt.defaultPrevented); // true }); });
 #target { margin-top: 600vh; }
 <a href="#target">go to target</a> <div id="target">target</div>

However, in the case of a synthetic event fired by JS, that event is dispatched synchronously and the JS stack is never empty (since it at least contains the job that did fire the event in the first place).
So when the browser has to perform the " clean up after running script " algorithm after it called each of our JS callbacks, it will not perform a microtask-checkpoint this time.
Instead, it will continue until the step 12 of the dispatch an event algorithm with the cancelled flag still down, and will execute the default action. Only after this, it will return from whatever did fire that synthetic event, and only after that script will get cleaned after the browser will be able to execute the microtask-checkpoint.

In the following snippet I'll use an <input type="checkbox"> element because its activation behavior is synchronous, whereas <a> 's "navigate a link" isn't and thus doesn't make for a good example.

 const input = document.querySelector("input"); input.addEventListener("click", (evt) => { console.log("event fired"); Promise.resolve().then( () => { console.log("microtask fired, preventing"); evt.preventDefault(); }); }); console.log("before click, is checkbox checked:", input.checked); input.click(); console.log("after click, is checkbox checked:", input.checked);
 #target { margin-top: 600vh; }
 <input type="checkbox"> <div id="target">target</div>


So now we just said what Jake said in his presentation.

Let's make it a bit more focused on OP's case who want to still be able to deal with their event handler as a Promise, and want to be able to prevent the default behavior of the event.

This is not possible.

For an object to work with Promises as a Promise its callback has to get executed in a microtask-checkpoint. As we've seen above, the microtask-checkpoint will only get performed after the default behavior has been executed. So this is a dead-end.

What could be done:

  • prevent the default behavior in the event handler rather than in the Promise reaction:

     const link = document.querySelector("a"); const prom = new Promise( (resolve) => { link.addEventListener("click", (evt) => { evt.preventDefault(); resolve(evt); }, { once: true } ); }); prom.then( (evt) => console.log("clicked") ); // true link.click();
     #target { margin-top: 600vh; }
     <a href="#target">go to target</a> <div id="target">target</div>
    But this means that either you prevent all default behaviors, either you move some logic inside the even handler, maybe defeating the idea of having a Promise there in the first place.
    Still, if you just want something to be chained after this event, this might be a viable solution.

  • Don't use a real Promise but just something with a similar API, that will execute the callback synchronously:

     class EventReaction { constructor( executor ) { this._callbacks = []; executor( this._resolver.bind( this ) ); } after( callback ) { if( this._done ) { callback(); } else { this._callbacks.push( callback ); } } _resolver( arg ) { this._callbacks.forEach( cb => cb( arg ) ); this._callbacks.length = 0; } } const link = document.querySelector("a"); const event_reaction = new EventReaction( (resolve) => { link.addEventListener("click", (evt) => { resolve(evt); }, { once: true } ); } ); event_reaction.after( (evt) => { console.log("preventing"); evt.preventDefault(); }); link.click();
     #target { margin-top: 600vh; }
     <a href="#target">go to target</a> <div id="target">target</div>
    But this is not a Promise, and can't be chained with a Promise nor used by any of the Promise's methods.

Now, the call is yours.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM