Ashwin
Ashwin

Reputation: 963

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..

Upvotes: 4

Views: 1300

Answers (4)

avasuro
avasuro

Reputation: 76

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);
    }); 
    

Upvotes: 0

pesho hristov
pesho hristov

Reputation: 2060

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` .. 

    });
};

Upvotes: 0

MiF
MiF

Reputation: 678

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.

Upvotes: 0

gu mingfeng
gu mingfeng

Reputation: 1050

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

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

Upvotes: 1

Related Questions