Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Click event not firing when React Component in a Shadow DOM

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

like image 474
josephnvu Avatar asked Jun 16 '16 17:06

josephnvu


People also ask

Does react work with shadow DOM?

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.

Can you add onClick to react component?

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.

Can we access the element which is inside closed shadow DOM?

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

How do I check shadow DOM?

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.


2 Answers

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

like image 98
josephnvu Avatar answered Sep 21 '22 22:09

josephnvu


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

like image 20
Lukas Avatar answered Sep 21 '22 22:09

Lukas