8

I'd like to set a custom property on a click event in a listener, and read this property in another event listener further up:

document.body.addEventListener('click', function(e) {
    if (e.someCustomProperty) {
        // ...
    }
});

document.getElementById('some-element').addEventListener('click', function(e) {
    e.someCustomProperty = someValue;
});

This seems to work fine in the latest version of all the browsers I could try:

  • Windows: IE11, Edge, Chrome, Firefox
  • iPhone: Safari, Edge, Chrome, Firefox

The event object retains the custom property when bubbling up.

This makes me quite confident that this behaviour is widely supported, but I could not find any envidence that this is standard though.

Can I trust that this behaviour works, and will continue to work, in all modern browsers?

If not, what is the standard way to do this?


I found a similar question from 2011, but the only answer is quite outdated now, mentioning it not working on IE8 (which I don't care today).


Background

Maybe I should give a little background to what I'm trying to achieve, and why the alternatives offered below - I think - do not help:

I have a modal window, that's basically a fixed DIV at the center of the screen:

<body>
    <div id="modal">
        <!-- content -->
    </div>
</body>

When the modal is open, I setup a click listener on the body, that checks if a click has happened inside or outside the modal:

  • if the click happened inside the modal, the listener does nothing
  • if the click happened outside the modal, the listener closes the modal

Hence the following code:

document.body.addEventListener('click', function(e) {
    if (! e.insideModal) {
        modal.close();
    }
});

document.getElementById('modal').addEventListener('click', function(e) {
    e.insideModal = true;
});

Failed alternatives:

  • I can't just stopPropagation() inside the modal click listener, as other listeners on body must still be able to catch the click;
  • I can't use document.getElementById('modal').contains(e.target) inside the body click listener, as by the time the click bubbles up to the body, the content of the modal may have already changed, and the click target may be gone;
  • I can't use a property on window, in the target element's dataset or anywhere else, as I fail to see how that will help me know whether this particular click has hit the modal before hitting the body; if you do, feel free to chime in!

This is why the event looks like the best place to put this information.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
  • In my understanding `e` in both the handler will be different – brk Oct 17 '18 at 16:09
  • @brk They're the exact same object (`===`), I just tested this in Chrome by setting the event as a `window` property in a listener, and comparing it in the other listener. – BenMorel Oct 17 '18 at 16:14
  • @zfrisch Why would the xy coordinates change? We're talking about the same click! Is there any property that could change during bubbling? – BenMorel Oct 17 '18 at 16:15
  • Here is a good read: http://perfectionkills.com/whats-wrong-with-extending-the-dom/ .... and this might be even better: http://lea.verou.me/2015/04/idea-extending-native-dom-prototypes-without-collisions/ – Asons Oct 17 '18 at 17:56
  • Possible duplicate of https://stackoverflow.com/questions/779880/in-javascript-can-you-extend-the-dom – Asons Oct 17 '18 at 17:59
  • Personally, I create myself a global object and assign/remove properties/data/value to it, and with that have it accessible everywhere ... and can be stored between page load using e.g. local storage (stringified). – Asons Oct 17 '18 at 18:12
  • @LGSon What you're linking to is a whole different subject, I'm not talking about extending the event prototype, but adding a custom property to one event instance only. Further events will not have this property set automatically. About your suggestion, please let me know how this would fit my use case, I just updated my question. – BenMorel Oct 17 '18 at 19:21
  • Using `stopPropagation()` doesn't prevent other listeners to bubble, only the one occurred on the in this case `getElementById('modal')` element, so with the given sample code, just do `e.stopPropagation()` instead of `e.insideModal = true;` If there is other elements that should bubble, you can make sure a certain element was the one clicked by compare the event target with _this_, e.g. `e.target == this`. That will allow click on a descendant to pass/be treated differently. – Asons Oct 17 '18 at 19:40
  • And btw, what I linked to were more to show what other recommends when it comes to custom properties and what can go wrong, and as you can see, more or less all of them say _avoid custom properties_, or do it properly. And what you do is actually extend the event object the second you assign it a custom property. – Asons Oct 17 '18 at 19:49
  • As stated in my updated question, I cannot use `stopPropagation()`, as it does prevent the event from bubbling, and other listeners further up in the document do not receive the click event. This is the whole purpose of `stopPropagation()`, and in my case, it breaks other functionality. – BenMorel Oct 17 '18 at 20:31
  • Then edit your sample code so we can see what other listeners further up needs. – Asons Oct 17 '18 at 20:52

1 Answers1

1

In order to make modal window close on click outside a very simple technique can be used: modal window is wrapped with a transparent overlay that covers all the screen and has an onclick event listener inside which it compares event.target(an element which emits click event) and event.currentTarget (an element which has event listener appended). Equality means the overlay was clicked. Inquality means the modal was clicked.

function openModal() {
    document.getElementById('overlay').classList.add('visible');
}

document.getElementById('overlay').addEventListener('click', event => {
    if(event.target === event.currentTarget){
       event.target.classList.remove('visible');
    }
});
.overlay {
  display: none;
  background: transparent;
  height: 100vh;
  width: 100vw;
  position: fixed;
  top: 0;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.overlay.visible {
  display: flex
}

.modal {
  width: 100px;
  height: 100px;
  background: grey;
}
<button onclick="openModal()">Open modal</button>
<div id="overlay" class="overlay">
   <div class="modal"/>
</div>
zb'
  • 8,071
  • 4
  • 41
  • 68
Volodymyr
  • 1,360
  • 1
  • 7
  • 12
  • The problem with this approach is that I can't easily tell which click we're talking about. See the update to my question to understand the problem: the `body` listener needs to know whether the click it receives has hit a given element before bubbling up to itself. – BenMorel Oct 17 '18 at 19:15
  • You're right, adding a covering element is probably the simplest thing to do, to make it possible to click only one possible element outside the modal. – BenMorel Oct 17 '18 at 20:57
  • I'll accept your answer, even though we digressed from the original question. My tests made me quite confident that setting a custom property on an event works reliably, but the fact that it is not documented, and that the Event does not seem to have a built-in mechanism to store user data, does make it feel wrong somehow. Let's stick with proven solutions wherever possible. – BenMorel Oct 17 '18 at 21:00