web-dev-qa-db-ja.com

ReactコンポーネントがShadowDOMにあるときにクリックイベントが発生しない

ReactコンポーネントをWebコンポーネントでカプセル化する必要がある特別なケースがあります。セットアップは非常に簡単に思えます。Reactコード:

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

これは正常にマウントされ、クリックイベントが機能します。 React Componentの周りにWebコンポーネントラッパーを作成すると、正しくレンダリングされますが、クリックイベントが機能しません。Webコンポーネントラッパーは次のとおりです。

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

そしてここにHTMLがあります:

<box-webcomp></box-webcomp>

足りないものはありますか?または、React Webコンポーネント内での作業を拒否しますか?この種のことを行うMaple.JSのようなライブラリを見たことがありますが、それらのライブラリは機能します。小さなライブラリが1つ欠けているように感じます。事。

これがCodePenなので、問題を確認できます。

http://codepen.io/homeslicesolutions/pen/jrrpLP

22
josephnvu

結局のところ、Shadow DOMはクリックイベントを再ターゲットし、イベントをシャドウにカプセル化します。 ReactはShadowDOMをネイティブにサポートしていないため、これが気に入らないため、イベントの委任はオフになり、イベントは発生しません。

私がやろうと決めたのは、技術的に「光の中に」ある実際のシャドウコンテナにイベントを再バインドすることでした。 event.pathを使用してイベントのバブリングを追跡し、コンテキスト内のすべてのReactイベントハンドラーをシャドウコンテナーまで起動します。

可能なすべてのイベントタイプをコンテナにバインドする「retargetEvents」メソッドを追加しました。次に、「__ reactInternalInstances」を見つけて正しいReactイベントをディスパッチし、イベントスコープ/パス内のそれぞれのイベントハンドラーを探します。

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

      });
    });
  }

ReactコンポーネントをシャドウDOMにレンダリングするときに、「retargetEvents」を実行します

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

これがReactの将来のバージョンで機能することを願っています。これが機能しているcodePenです:

http://codepen.io/homeslicesolutions/pen/ZOpbWb

これを修正する方法の手がかりを与えてくれたリンクを@mrlewに感謝し、私と同じ波長について考えてくれた@Wildhoneyにも感謝します=)。

19
josephnvu

@ josephvnuの受け入れられた回答 のコードをクリーンアップしたバグを修正しました。ここでnpmパッケージとして公開しました: https://www.npmjs.com/package/react-shadow-dom-retarget-events

使い方は次のとおりです

インストール

yarn add react-shadow-dom-retarget-eventsまたは

npm install react-shadow-dom-retarget-events --save

使用

retargetEventsをインポートし、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});

参考までに、これは修正の完全なソースコードです https://github.com/LukasBombach/react-shadow-dom-retarget-events/blob/master/index.js

6
Lukas

私は偶然に別の解決策を発見しました。 reactの代わりにpreact-compatを使用してください。 ShadowDOMでは正常に機能しているようです。 Preactはイベントに異なる方法でバインドする必要がありますか?

0
William Hilton

this.el = this.createShadowRoot();this.el = document.getElementById("mountReact");に置き換えるだけでうまくいきました。おそらく、reactにはグローバルイベントハンドラーがあり、shadowdomはイベントのリターゲティングを意味するためです。

0
mrlew