0

In my client code, I have

const whatMouseDid = await mouseDoesSomething()

with the latter function looking like:

async mouseDoesSomething() {
    const mouseUp = (resolve) => {
        const handler = (evt) => {
            if (evt.button === 0)
                resolve("up")
        }
        return handler
    }
    const mouseDown = (resolve) => {
        const handler = (evt) => {
            if (evt.button === 0)
                resolve("down")
        }
        return handler
    }
    return new Promise((resolve, reject) => {
        document.addEventListener('mouseup', mouseUp(resolve))
        document.addEventListener('mousedown', mouseDown(resolve))
    })
}

Which is already a bit more convoluted than I'd prefer, but still manageable. However, there's a problem - the listeners are never removed. And because I need to pass in a reference to Promise.resolve, I can't removeEventListener easily. The only way I can think to do this is to keep a mutable list of handlers and the events, targets, etc they are assigned to, and in the handler function(s), iterate over that list and remove attached handlers. The optional param {once: true} also won't work because I don't resolve if the button clicked is not the one I want.

This all feels super convoluted, and makes me think I'm just missing the obvious easy way to do this; am I? Or is it really this much of nuisance?

Rollie
  • 4,391
  • 3
  • 33
  • 55

3 Answers3

2

And because I need to pass in a reference to Promise.resolve, I can't removeEventListener easily. The only way I can think to do this is to keep a mutable list

Why so complicated? You create functions in the promise executor, just reference them.

mouseDoesSomething() {
    return new Promise((resolve, reject) => {
        const mouseUphandler = evt => {
            if (evt.button === 0) {
                resolve("up")
                document.removeEventListener('mouseup', mouseUpHandler)
                document.removeEventListener('mousedown', mouseDownHandler)
            }
        }
        const mouseDownhandler = evt => {
            if (evt.button === 0) {
                resolve("down")
                document.removeEventListener('mouseup', mouseUpHandler)
                document.removeEventListener('mousedown', mouseDownHandler)
            }
        }
        document.addEventListener('mouseup', mouseUpHandler)
        document.addEventListener('mousedown', mouseDownHandler)
    })
}

Surely that's lots of code duplication, but you can abstract this again by creating the handlers dynamically - you just need to do it in the scope where the two handlers are declared:

mouseDoesSomething() {
    return new Promise((resolve, reject) => {
        const makeHandler = dir => evt => {
            if (evt.button === 0) {
                resolve(dir)
                document.removeEventListener('mouseup', mouseUpHandler)
                document.removeEventListener('mousedown', mouseDownHandler)
            }
        }
        const mouseUphandler = makeHandler("up")
        const mouseDownhandler = makeHandler("down")
        document.addEventListener('mouseup', mouseUpHandler)
        document.addEventListener('mousedown', mouseDownHandler)
    })
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • That is better :) I always expect that I need names to be defined before I can use them, so I discarded similar ideas when working through it on my own. – Rollie Jan 21 '21 at 02:32
  • That's the [magic](https://stackoverflow.com/a/52880419/1048572) of [hoisting](https://stackoverflow.com/a/32475965/1048572) :-) I couldn't write code without it. – Bergi Jan 21 '21 at 02:36
2

You could create a helper function to call when you resolve your function:

mouseDoesSomething() {
  return new Promise((resolve, reject) => {
    const mouseUp = (evt) => {
      if (evt.button === 0) resolveAndRemove("up");
    };
    const mouseDown = (evt) => {
      if (evt.button === 0) resolveAndRemove("down");
    };

    function resolveAndRemove(val) {
      document.removeEventListener("mouseup", mouseUp);
      document.removeEventListener("mousedown", mouseDown);
      resolve(val);
    }

    document.addEventListener("mouseup", mouseUp);
    document.addEventListener("mousedown", mouseDown);
  });
}

Also, since you're not using await, you don't need to mark the function as async.

cbr
  • 12,563
  • 3
  • 38
  • 63
  • Ah, that's not bad - I thought I couldn't reference a function before it was declared, but I guess that's not the case. – Rollie Jan 21 '21 at 02:30
2

Previous answers are fine, just a 2 cents answer for the (close) future:

EventTarget.addEventListener now accepts an AbortSignal which you can use to remove the event listener when needed.

So in Firefox Nightly (86) and in Chrome Canary(90) you can do

async function mouseDoesSomething() {
  return new Promise( (resolve, reject) => {
    let count = 0;
    const controller = new AbortController();
    const onevent = (evt) => {
      console.log( ++count );
      if( count >= 5 ) {
        resolve();
        controller.abort();
      }
    };
    document.addEventListener( 'mouseup', onevent, { signal: controller.signal } );
    document.addEventListener( 'mousedown', onevent, { signal: controller.signal } );
  } );
}
mouseDoesSomething().then( () => console.log('done') );

To detect for support you can use the usual property-bag trap:

const support = (() => {
  let support = false;
  const trap = {};
  Object.defineProperty(trap, "signal", { get() { support = true; } } );
  new EventTarget().addEventListener('foo', ()=>{}, trap);
  return support;
})();

console.log( support );
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • This is really what I was hoping for when I started investigating! It felt really weird to me that I couldn't do something like `event.context.removeSelf()`. Look forward to this being widely available - thanks for sharing :) – Rollie Jan 21 '21 at 03:29