I'm in a situation, where I kind of have to write my own event bubbling. Therefore, I need to pass a given Event-Object (created originally from the Browser/DOM) to multiple functions/instances, but I need to change the .target
property for instance.
Problem: The property descriptors of almost all attributes deny any mutation, deletion or change. Furthermore, it seems not to be possible to clone or copy the whole Event-Object using Object.assign
or read in the attributes using Object.getOwnPropertyNames
or Object.keys
.
It is of course possible to manually copy all properties into a new object by assigning them, but that seems pretty foolish.
I tried to be really smart and just use the original event-object as prototype of a new object, to just shadow the properties I want to mutate. Like
Object.setPrototypeOf( customEvent, event );
But if I do that, any access to properties of customEvent
throw with:
Uncaught TypeError: Illegal invocation
It does seem to work if we use a Proxy-handler which also can be used to shadow certain properties. The only problem I have here is, that it is still not possible to execute functions like stopPropagation
over the Proxy (Uncaught TypeError: Illegal invocation again).
Question: What is the best practice to manipulate/extend a DOM Event Object?
You will suffer from 7 years of misfortune if you mention jQuery in any form or shape.
I have used a Proxy
for this problem, combined with a wrapper.
First, setup a function that acts as a wrapper around a proxy. The wrapper receives an Event object and sets its value for later use. The reason for this will be explained further down.
function AdaptedEvent(ev) {
const initialEvent = ev;
let newTarget = 'your new target'; // can be dynamic if you wish.
// returning with a handler for proxy
return {
get: (target, prop) => {
// proxy'ing stuff
}
};
}
then inside the proxy, I have
get: (target, prop) => {
if (prop === 'target') {
return newTarget ? newTarget : initialEvent.target;
}
return target[prop];
}
when .target
is called on your proxy object, it will do the statements described in the above if-body. It will return with a new target instead of the event's initial target.
Once you have set up your proxy, you have to construct a proxy object when handling a click event. Like
document.querySelector('#foo').addEventListener(
'click',
e => handleClick(new Proxy(e, new AdaptedEvent(e)))
);
Example:
function AdaptedEvent(ev) {
const initialEvent = ev;
let newTarget = 'your new target';
return {
get: (target, prop) => {
if (prop === 'target') {
return newTarget ? newTarget : initialEvent.target;
}
return target[prop];
}
};
}
function handleClick(event) {
console.log(event.target);
console.log(event.which);
}
// binding click event.
document.querySelector('#foo').addEventListener('click', e => handleClick(new Proxy(e, new AdaptedEvent(e))));
<button id="foo">click me!</button>
But as you have mentioned an issue that I have seen too when trying it out:
It does seem to work if we use a Proxy-handler which also can be used to shadow certain properties. The only problem I have here is, that it is still not possible to execute functions like
stopPropagation
over the Proxy (Uncaught TypeError: Illegal invocation again).
In the example here below; I have replaced a button to a link. Let's say I want to stop its bubbling by using .preventDefault()
. You can see that it will throw the mentioned error.
function AptEvent(ev) {
const initialEvent = ev;
let newTarget = 'your new target';
return {
get: (target, prop) => {
if (prop === 'target') {
return newTarget ? newTarget : initialEvent.target;
}
return target[prop];
}
};
}
function handleClick(event) {
event.preventDefault();
console.log(event.target);
console.log(event.which);
}
// binding click event.
document.querySelector('#foo').addEventListener('click', e => handleClick(new Proxy(e, new AptEvent(e))));
<a id="foo" href="www.google.com">click me!</a>
The reason for this is that when calling the said function, you have lost the right context to call preventDefault
. A solution for this is to use .bind
. But what should you pass to the .bind
? Yes, the native Event
object. That is why I have setup the above AdaptedEvent
function that receives an Event object as argument. Now you can pass the object when the call to a property is a function (easily being checked).
When you click on the link, the event bubbling got stopped.
function AdaptedEvent(ev) {
const initialEvent = ev;
let newTarget = 'your new target';
return {
get: (target, prop) => {
if (Object.prototype.toString.call(target[prop]) === '[object Function]') {
return target[prop].bind(initialEvent);
}
else if (prop === 'target') {
return newTarget ? newTarget : initialEvent.target;
}
return target[prop];
}
};
}
function handleClick(event) {
event.preventDefault();
console.log(event.target);
console.log(event.which);
}
// binding click event.
document.querySelector('#foo').addEventListener('click', e => handleClick(new Proxy(e, new AdaptedEvent(e))));
<a id="foo" href="www.google.com">click me!</a>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With