当阴影中的 React 组件 DOM 时,单击事件未触发
Click event not firing when React Component in a Shadow DOM
我有一个特殊情况,我需要用 Web Component 封装 React Component。设置看起来非常简单。这是 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,所以你可以看到问题:
用 this.el = document.getElementById("mountReact");
替换 this.el = this.createShadowRoot();
刚刚奏效。可能是因为 React 有一个全局事件处理程序和阴影 dom 意味着事件重定向。
事实证明,Shadow DOM 重新定位点击事件并将事件封装在 shadow 中。 React 不喜欢这样,因为它们本身不支持 Shadow DOM,因此事件委托已关闭并且事件不会被触发。
我决定做的是将事件重新绑定到技术上 "in the light" 的实际影子容器。我使用 event.path
跟踪事件的冒泡,并触发上下文中的所有 React 事件处理程序直到影子容器。
我添加了一个 'retargetEvents' 方法,它将所有可能的事件类型绑定到容器。然后它将通过找到“__reactInternalInstances”并在事件 scope/path.
中寻找相应的事件处理程序来调度正确的 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 组件渲染到阴影中时,我会执行 "retargetEvents" 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();
}
};
}
我希望这适用于 React 的未来版本。这是它工作的codePen:
http://codepen.io/homeslicesolutions/pen/ZOpbWb
感谢@mrlew 的link,它给了我如何解决这个问题的线索,也感谢@Wildhoney 与我在相同波长上的思考=)。
我修复了清理 . I published it as an npm package here: 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
我偶然发现了另一个解决方案。使用 preact-compat
而不是 react
。似乎在 ShadowDOM 中运行良好; Preact 必须以不同方式绑定到事件?
此答案是五年后的更新。
坏消息:@josephnvu 的回答(在撰写本文时已接受)和 react-shadow-dom-retarget-events
包不再正常工作,至少在 React 16.13.1 中是这样——尚未对早期版本进行测试。看起来 React 内部发生了一些变化,导致代码调用了错误的侦听器回调。
好消息:
- 在 React 16.13.1 中(同样,未使用早期 16.x 进行测试),可以直接渲染到 shadow root 中,而无需中间块。在这种情况下,侦听器将附加到影子根而不是文档,因此 React 能够正确捕获和分发所有事件。明显的权衡是你不能向同一个影子根添加任何其他东西,因为 React 会用渲染的 JSX 覆盖你的元素。
- 在 React 17 中,React 将其侦听器附加到渲染根,而不是文档或影子根,因此无论我们渲染到哪里,一切都开箱即用。
我有一个特殊情况,我需要用 Web Component 封装 React Component。设置看起来非常简单。这是 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,所以你可以看到问题:
用 this.el = document.getElementById("mountReact");
替换 this.el = this.createShadowRoot();
刚刚奏效。可能是因为 React 有一个全局事件处理程序和阴影 dom 意味着事件重定向。
事实证明,Shadow DOM 重新定位点击事件并将事件封装在 shadow 中。 React 不喜欢这样,因为它们本身不支持 Shadow DOM,因此事件委托已关闭并且事件不会被触发。
我决定做的是将事件重新绑定到技术上 "in the light" 的实际影子容器。我使用 event.path
跟踪事件的冒泡,并触发上下文中的所有 React 事件处理程序直到影子容器。
我添加了一个 'retargetEvents' 方法,它将所有可能的事件类型绑定到容器。然后它将通过找到“__reactInternalInstances”并在事件 scope/path.
中寻找相应的事件处理程序来调度正确的 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 组件渲染到阴影中时,我会执行 "retargetEvents" 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();
}
};
}
我希望这适用于 React 的未来版本。这是它工作的codePen:
http://codepen.io/homeslicesolutions/pen/ZOpbWb
感谢@mrlew 的link,它给了我如何解决这个问题的线索,也感谢@Wildhoney 与我在相同波长上的思考=)。
我修复了清理
用法如下
安装
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
我偶然发现了另一个解决方案。使用 preact-compat
而不是 react
。似乎在 ShadowDOM 中运行良好; Preact 必须以不同方式绑定到事件?
此答案是五年后的更新。
坏消息:@josephnvu 的回答(在撰写本文时已接受)和 react-shadow-dom-retarget-events
包不再正常工作,至少在 React 16.13.1 中是这样——尚未对早期版本进行测试。看起来 React 内部发生了一些变化,导致代码调用了错误的侦听器回调。
好消息:
- 在 React 16.13.1 中(同样,未使用早期 16.x 进行测试),可以直接渲染到 shadow root 中,而无需中间块。在这种情况下,侦听器将附加到影子根而不是文档,因此 React 能够正确捕获和分发所有事件。明显的权衡是你不能向同一个影子根添加任何其他东西,因为 React 会用渲染的 JSX 覆盖你的元素。
- 在 React 17 中,React 将其侦听器附加到渲染根,而不是文档或影子根,因此无论我们渲染到哪里,一切都开箱即用。