简体   繁体   中英

Handle React event delegation with stopPropagation()

I have a project in React which should be able to be placed on any website. The idea is that I host a javascript file, people place a div with a specific ID, and React renders in that div.

So far this works, except click-events. These evens are handled at the top level . This is all good, but one of the sites where the app should be placed, has stopPropagation() implemented for a off-canvas menu. Because of this the events aren't working properly.

I tried catching all events at the root-element, and dispatching them manually:

this.refs.wrapper.addEventListener('click', (event) => {
    console.log(event);
    console.log(event.type);
    const evt = event || window.event;
    evt.preventDefault();
    evt.stopImmediatePropagation();
    if (evt.stopPropagation) evt.stopPropagation();
    if (evt.cancelBubble !== null) evt.cancelBubble = true;
    document.dispatchEvent(event);
});

This doesn't work, because the event is already being dispatched:

Uncaught InvalidStateError: Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.

What would be the right way to fix this problem? Not using the synthetic events from React doesn't seem the right way to go for me..

Argument 'event h'as already been dispatched. You should clone a new eventobject with old event.

var newevent = new event.constructor(event.type, event)

Ther is no solution yet. React, as you say, listen events on the root of DOM, and filter events if their event.target not inside react's mounted node.
You can try:
1. Redispatch new event in the Reract component, but it will be stopped at outside handler too.
2. Dispatch new event outside Reract component, higher (closest to BODY ) then node with stopPropagation callback. But event.target will point to node, which not inside React's component and you can not change it, beacause it is readonly.
Maybe in next versions they will fix it.

But you can listen for events in the document, no?

Let say your root component for the whole app is named app . Then, inside it's componentDidMount you can have:

// when the main App component mounts - we'll add the event handlers ..
componentDidMount() {

    var appComponent = this;

    document.addEventListener('click', function(e) {

        var clickedElement = e.target;

        // Do something with the clickedElement - by ID or class .. 
        // You'll have reference to the top level component in `appComponent` .. 

    });
};

As you said - React handles all events at the top-level node (document), and to determine which react component relates to some event react uses event.target property. So to make everything work you should manually dispatch stopped event on document node and set proper "target" property to this event.

There is 2 problems to solve:

  • You can't trigger event that is already dispatched. To solve this you have to create a fresh copy of this event.
  • After you do dispatchEvent() on some node browser automatically set "target" property of this event to be the node on which event is fired. To solve this you should set proper target before dispatchEvent(), and make this property read-only using property descriptors.

General solution:

Solution tested in all modern browsers and IE9+

Here is a source code of solution: https://jsbin.com/mezosac/1/edit?html,css,js,output . (Sometimes it hangs, so if you don't see UI elements in preview area - click on "run win js" button on top right corner)

It is well commented, so I will not describe here all of that stuff, but I will quickly explain main points:

  1. Event should be redispatched immediately after it was stopped, to achieve this I extended native stopPropagation and stopImmediatePropagation methods of event to call my redispatchEventForReact function before stopping propagation:

     if (event.stopPropagation) { const nativeStopPropagation = event.stopPropagation; event.stopPropagation = function fakeStopPropagation() { redispatchEventForReact(); nativeStopPropagation.call(this); }; } if (event.stopImmediatePropagation) { const nativeStopImmediatePropagation = event.stopImmediatePropagation; event.stopImmediatePropagation = function fakeStopImmediatePropagation() { redispatchEventForReact(); nativeStopImmediatePropagation.call(this); }; }

    And there is another one possibility to stop event - setting "cancelBubble" property to "true". If you take a look at cancalBubble property descriptor - you will see that this property indeed is a pair of getters/setters, so it's easy to inject "redispatchEventForReact" call inside setter using Object.defineProperty:

     if ('cancelBubble' in event) { const initialCancelBubbleDescriptor = getPropertyDescriptor(event, 'cancelBubble'); Object.defineProperty(event, 'cancelBubble', { ...initialCancelBubbleDescriptor, set(value) { redispatchEventForReact(); initialCancelBubbleDescriptor.set.call(this, value); } }); }
  2. redispatchEventForReact function:

    2.1 Before we dispatch event for react we should remove our customized stopPropagation and stopImmediatePropagation methods (because in react code some component in theory can invoke e.stopPropagation, which will trigger redispatchEventForReact again, and this will lead to infinite loop):

     delete event.stopPropagation; delete event.stopImmediatePropagation; delete event.cancelBubble;

    2.2 Then we should make a copy of this event. It's easy to do in modern browsers, but take a looot of code for IE11-, so I moved this logic in separate function (see attached source code on jsbin for details):

     const newEvent = cloneDOMEvent(event);

    2.3 Because browser set "target" property of event automatically when event is dispatched we should make it read-only. Important bit here - setting value and writeable=false will not work in IE11-, so we have to use getter and empty setter:

     Object.defineProperty(newEvent, 'target', { enumerable: true, configurable: false, get() { return event.target; }, set(val) {} });

    2.4 And finally we can dispatch event for react:

     document.dispatchEvent(newEvent);
  3. To guarantee that hacks for react will be injected in event before something stopped this event we should listen to this event on root node in capturing phase and do injections:

     const EVENTS_TO_REDISPATCH = ['click']; EVENTS_TO_REDISPATCH.forEach(eventToRedispatch => { document.addEventListener(eventToRedispatch, prepareEventToBeRedispatched, true); });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM