I have a special case where I need to encapsulate a React Component with a Web Component. The setup seems very straight forward. Here is the React Code:
// React Component class Box extends React.Component { handleClick() { alert("Click Works"); } render() { return ( <div style={{background:'red', margin: 10, width: 200, cursor: 'pointer'}} onClick={e => this.handleClick(e)}> {this.props.label} <br /> CLICK ME </div> ); } }; // Render React directly ReactDOM.render( <Box label="React Direct" />, document.getElementById('mountReact') );
HTML:
<div id="mountReact"></div>
This mounts fine and the click event works. Now when I created a Web Component wrapper around the React Component, it renders correctly but the click event doesn't work. Here is my Web Component Wrapper:
// Web Component Wrapper class BoxWebComponentWrapper extends HTMLElement { createdCallback() { this.el = this.createShadowRoot(); this.mountEl = document.createElement('div'); this.el.appendChild(this.mountEl); document.onreadystatechange = () => { if (document.readyState === "complete") { ReactDOM.render( <Box label="Web Comp" />, this.mountEl ); } }; } } // Register Web Component document.registerElement('box-webcomp', { prototype: BoxWebComponentWrapper.prototype });
And here is the HTML:
<box-webcomp></box-webcomp>
Is there something I'm missing? Or does React refuse to work inside a Web Component? I have seen a library like Maple.JS which does this sort of thing, but their library works. I feel like I'm missing one small thing.
Here is the CodePen so you can see the problem:
http://codepen.io/homeslicesolutions/pen/jrrpLP
Introduction. React shadow root allows you to use the shadow DOM in your React components. The biggest advantage of this is that you can include your CSS with your component and it will be scoped to the shadow DOM. The styles don't leak out of the shadow DOM and only inheritable styles can't get in.
The React onClick event handler enables you to call a function and trigger an action when a user clicks an element, such as a button, in your app. Event names are written in camelCase, so the onclick event is written as onClick in a React app. In addition, React event handlers appear inside curly braces.
Any code is able to access the shadow tree of elem . "closed" – elem. shadowRoot is always null . We can only access the shadow DOM by the reference returned by attachShadow (and probably hidden inside a class).
To identify Shadow DOM: In Google Chrome, navigate to https://shop.polymer-project.org. Open Developer tools (press the shortcut keys Fn+F12). On the Elements tab, expand the <body> element and the first element inside the <body> element and notice the #shadow-root line.
As it turns out the Shadow DOM retargets click events and encapsulates the events in the shadow. React does not like this because they do not support Shadow DOM natively, so the event delegation is off and events are not being fired.
What I decided to do was to rebind the event to the actual shadow container which is technically "in the light". I track the event's bubbling up using event.path
and fire all the React event handlers within context up to the shadow container.
I added a 'retargetEvents' method which binds all the possible event types to the container. It then will dispatch the correct React event by finding the "__reactInternalInstances" and seek out the respective event handler within the event scope/path.
retargetEvents() { let events = ["onClick", "onContextMenu", "onDoubleClick", "onDrag", "onDragEnd", "onDragEnter", "onDragExit", "onDragLeave", "onDragOver", "onDragStart", "onDrop", "onMouseDown", "onMouseEnter", "onMouseLeave","onMouseMove", "onMouseOut", "onMouseOver", "onMouseUp"]; function dispatchEvent(event, eventType, itemProps) { if (itemProps[eventType]) { itemProps[eventType](event); } else if (itemProps.children && itemProps.children.forEach) { itemProps.children.forEach(child => { child.props && dispatchEvent(event, eventType, child.props); }) } } // Compatible with v0.14 & 15 function findReactInternal(item) { let instance; for (let key in item) { if (item.hasOwnProperty(key) && ~key.indexOf('_reactInternal')) { instance = item[key]; break; } } return instance; } events.forEach(eventType => { let transformedEventType = eventType.replace(/^on/, '').toLowerCase(); this.el.addEventListener(transformedEventType, event => { for (let i in event.path) { let item = event.path[i]; let internalComponent = findReactInternal(item); if (internalComponent && internalComponent._currentElement && internalComponent._currentElement.props ) { dispatchEvent(event, eventType, internalComponent._currentElement.props); } if (item == this.el) break; } }); }); }
I would execute the "retargetEvents" when I render the React component into the shadow DOM
createdCallback() { this.el = this.createShadowRoot(); this.mountEl = document.createElement('div'); this.el.appendChild(this.mountEl); document.onreadystatechange = () => { if (document.readyState === "complete") { ReactDOM.render( <Box label="Web Comp" />, this.mountEl ); this.retargetEvents(); } }; }
I hope this works for future versions of React. Here is the codePen of it working:
http://codepen.io/homeslicesolutions/pen/ZOpbWb
Thanks to @mrlew for the link which gave me the clue to how to fix this and also thanks to @Wildhoney for thinking on the same wavelengths as me =).
I fixed a bug cleaned up the code of @josephvnu's accepted answer. I published it as an npm package here: https://www.npmjs.com/package/react-shadow-dom-retarget-events
Usage goes as follows
Install
yarn add react-shadow-dom-retarget-events
or
npm install react-shadow-dom-retarget-events --save
Use
import retargetEvents
and call it on the shadowDom
import retargetEvents from 'react-shadow-dom-retarget-events'; class App extends React.Component { render() { return ( <div onClick={() => alert('I have been clicked')}>Click me</div> ); } } const proto = Object.create(HTMLElement.prototype, { attachedCallback: { value: function() { const mountPoint = document.createElement('span'); const shadowRoot = this.createShadowRoot(); shadowRoot.appendChild(mountPoint); ReactDOM.render(<App/>, mountPoint); retargetEvents(shadowRoot); } } }); document.registerElement('my-custom-element', {prototype: proto});
For reference, this is the full sourcecode of the fix https://github.com/LukasBombach/react-shadow-dom-retarget-events/blob/master/index.js
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