Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mutate a DOM event object

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.

like image 742
jAndy Avatar asked Jan 15 '18 10:01

jAndy


Video Answer


1 Answers

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>
like image 102
KarelG Avatar answered Oct 18 '22 04:10

KarelG