19

I am working on a popup div and I would like to have a promise attached to the animation so I can do something after the popup ended.

My approach does not work because I could not find a way to pass the promise to the function on the event handler. Seems you cannot use bind here. I have tried and although I can resolve the promise, I cannot remove the event handler

What would be a different solution here?

function EventListenerForPopUp() {
    this.removeEventListener("animationend", EventListenerForPopUp );
    this.Show.resolve();
}   

function ShowHideDiv() {        
    this.Show = function () { 
        return new Promise(function(resolve, reject) {
            this.Div.addEventListener("animationend", EventListenerForPopUp, false);
        }.bind(this));
    }
}
punkrockbuddyholly
  • 9,675
  • 7
  • 36
  • 69
Jose Neto
  • 329
  • 1
  • 2
  • 8
  • addEventListener is used to set an event on an object or DOM element. It takes a callback, which is the "old" way of doing things. Isn't there a simple way to wrap addEventListener in a Promise, so that 'then' and 'catch' can be used? I would love to have this kind of consistency available in my programs. This question doesn't seem to have an acceptable answer as yet, even after 2 years. – David Spector Dec 16 '18 at 16:06
  • 1
    Of course, the obvious difference is that the same event can occur again and again, while a single asynchronous operation happens only once. Promises have a single Pending state which changes only once to a Fulfilled or Rejected state. This is not a good match with events, which occur repeatedly. It is also not a good match for setInterval, which occurs repeatedly, but is a good match for setTimeout, which occurs once. – David Spector Dec 16 '18 at 16:49

3 Answers3

27

You do not need to pass the promise to the event handler, you need to pass the resolve callback:

function EventListenerForPopUp(resolve) {
            this.removeEventListener("animationend", EventListenerForPopUp );
            resolve();
}   

// [...]

        return new Promise(function(resolve, reject) {
            this.Div.addEventListener("animationend", function() {
                EventListenerForPopUp.call(this, resolve);
            }, false);

This looks a bit ugly to me, maybe you can look at something like this:

var div = this.Div;
return new Promise(function (resolve) {
    div.addEventListener("animationend", function animationendListener() {
        div.removeEventListener("animationend", animationendListener);
        //call any handler you want here, if needed
        resolve();
    });
});
Volune
  • 4,324
  • 22
  • 23
6

Alternatively, we can create a reusable utility function as a means of creating a promise from any DOM event:

const createPromiseFromDomEvent = (eventTarget, eventName, run?) =>
    new Promise((resolve, reject) => {
        const handleEvent = () => {
            eventTarget.removeEventListener(eventName, handleEvent);

            resolve();
        };

        eventTarget.addEventListener(eventName, handleEvent);

        try {
            if (run) run();
        catch (err) {
            reject(err);
        }
    });

Example usage (with an MSE source buffer):

await createPromiseFromDomEvent(
    sourceBuffer,
    'update',
    () => sourceBuffer.remove(3, 10)
);

Depending on the situation, the run parameter may be needed to provide custom code to trigger the async operation (as above), or omitted if we know the operation has already started.

Patrick Kunka
  • 1,018
  • 8
  • 11
2

Another option is to abstract an externally controlled promise as a reusable function -

function thread () {
  let resolve, reject
  const promise = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })
  return [promise, resolve, reject]
}

function customPrompt(form) {
  const [prompt, resolve] = thread()
  form.yes.addEventListener("click", _ => resolve(true), {once: true})
  form.no.addEventListener("click", _ => resolve(false), {once: true})
  return prompt
}

customPrompt(document.forms.myform)
  .then(response => console.log("response:", response))
<form id="myform">
  <input type="button" name="yes" value="yes" />
  <input type="button" name="no" value="no" />
</form>

You can use async and await if you wanted -

async function main () {
  const response = await customPrompt(document.forms.myform)
  console.log("response:", response)
}

You could easily add a timeout by modifying customPrompt -

function customPrompt(form) {
  const [prompt, resolve, reject] = thread()
  form.yes.addEventListener("click", _ => resolve(true), {once: true})
  form.no.addEventListener("click", _ => resolve(false), {once: true})
  
  // timeout after 30 seconds
  setTimeout(reject, 30000, Error("no response"))
  return prompt
}

For other creative uses of thread, see this Q&A.

Mulan
  • 129,518
  • 31
  • 228
  • 259