影子 DOM 中的 React 组件时单击事件未触发
Posted
技术标签:
【中文标题】影子 DOM 中的 React 组件时单击事件未触发【英文标题】:Click event not firing when React Component in a Shadow DOM 【发布时间】:2016-10-18 09:16:57 【问题描述】:我有一个特殊情况,我需要用一个 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 组件创建 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 这样的库可以做这种事情,但是他们的库可以工作。我觉得我错过了一件小事。
这是 CodePen,因此您可以看到问题:
http://codepen.io/homeslicesolutions/pen/jrrpLP
【问题讨论】:
你说的这个特例是什么?只做实验吗? @Seth 只是一个概念证明,看看我们是否可以将 React 组件封装在 Web 组件中,以便我们可以在一些没有 React 作为主要框架的应用程序中使用它。这可能是一种遥不可及的方法,但只是想看看是否可行。 【参考方案1】:将this.el = this.createShadowRoot();
替换为this.el = document.getElementById("mountReact");
就可以了。也许是因为 react 有一个全局事件处理程序,而 shadow dom 意味着事件重定向。
【讨论】:
但我想使用shadow dom。否则不封装。这里的挑战是将 React 组件包装在自定义元素中。 我找到了this。也许值得一试。 感谢@mrlew。这部分有帮助:“由于 Shadow DOM 具有用于封装目的的 Event Retargeting 概念,事件委托将无法正常工作,因为所有事件似乎都来自 Shadow DOM - 因此 ReactShadow 使用每个元素的 React ID 来调度事件来自原始元素,因此维护 React 的事件委托实现。” 想出了一个解决方案。见上文。【参考方案2】:事实证明,Shadow DOM 会重新定位点击事件并将事件封装在阴影中。 React 不喜欢这样,因为它们本身不支持 Shadow DOM,因此事件委托已关闭并且事件不会被触发。
我决定做的是将事件重新绑定到实际的阴影容器,它在技术上是“在光线下”。我使用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 与我思考相同的波长 =)。
【讨论】:
很好的解决方案。想知道为什么反应团队不想整合它。我看到了两个关于这个的 PR。( 你会“开源”它吗?我发现了一些错误(很多 dispatchEvent 调用),修复它并分享会很好。 哦,伙计,我想给你 10.000 票。你救了我的命。 @Dimitry 和 josephnvu 我修复了一个错误,清理了代码并在npmjs.com/package/react-shadow-dom-retarget-events发布了这个解决方法 @Lukas 那是很久以前的事了,我不记得了。(那是我使用的脚本gist.github.com/DimitryDushkin/c091d5a6c33e10641eef0828261d5398,但好像 awwester 说的有一个错误。【参考方案3】:我修复了清理@josephvnu's accepted answer 代码的错误。我在这里将它作为 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的完整源代码
【讨论】:
【参考方案4】:我偶然发现了另一种解决方案。使用preact-compat
而不是react
。似乎在 ShadowDOM 中工作正常; Preact 必须以不同的方式绑定到事件?
【讨论】:
【参考方案5】:这个答案是五年后的更新。
坏消息:@josephnvu 的回答(在撰写本文时已被接受)和 react-shadow-dom-retarget-events
包不再正常工作,至少在 React 16.13.1 中 - 尚未在早期版本中进行测试。看起来 React 内部发生了一些变化,导致代码调用了错误的侦听器回调。
好消息:
在 React 16.13.1(同样,未在早期 16.x 中测试过)中,可以直接渲染到影子根中,而无需中间块。在这种情况下,侦听器将附加到影子根而不是文档,因此 React 能够正确捕获和调度所有事件。明显的权衡是你不能向同一个影子根添加任何其他东西,因为 React 会用渲染的 JSX 覆盖你的元素。 在 React 17 中,React 将其侦听器附加到渲染根,而不是文档或影子根,因此无论我们渲染到何处,一切都可以开箱即用。【讨论】:
以上是关于影子 DOM 中的 React 组件时单击事件未触发的主要内容,如果未能解决你的问题,请参考以下文章
在 Angular 2 中使用影子 DOM 时 Node.contains() 是不是有效?
是否可以将事件侦听器绑定到来自外部脚本的影子 dom 内的元素?