Reputation: 963
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
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:
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.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:
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);
}
});
}
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);
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
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
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
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