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
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
This answer is an update from five years after.
Bad news: answer by #josephnvu (accepted at the moment of writing) and the react-shadow-dom-retarget-events package no longer work correctly, at least with React 16.13.1 - haven't tested with earlier versions. Looks like something was changed in React internals, causing the code to invoke the wrong listener callback.
Good news:
In React 16.13.1 (again, not tested with earlier 16.x), it's possible to render directly into shadow root, without intermediate blocks. In this case, listeners would be attached to the shadow root and not to the document, so React is able to capture and dispatch all events correctly. The obvious tradeoff is that you can't add anything else to the same shadow root, since React will overwrite your elements with rendered JSX.
In React 17, React attaches its listeners to the rendering root, not to the document or shadow root, so everything works out of the box, no matter where we render to.
Replacing this.el = this.createShadowRoot(); with this.el = document.getElementById("mountReact"); just worked. Maybe because react has a global event handler and shadow dom implies event retargeting.
I've discovered another solution by accident. Use preact-compat instead of react. Seems to work fine in a ShadowDOM; Preact must bind to events differently?
Related
New to Vue and frameworks in general, and may have my thinking not very "Vue-like".
Trying to make a "super" button component that takes a prop, which dictates the buttons behavior so I only have to maintain one button component. The ideal form when implementing would like something like this...
<super-button isType="string"></super-button>
The template is...
<button class="buttonLow" v-bind:class="{buttonHigh: isSelected}">
<slot></slot>
</button>
Where isType prop could be momentary momentary-multi toggle or toggle-multi.
I have a basic set of event emitters/methods and listeners that work regardless of the isType and simply makes the buttons state high or low / on or off using another prop isSelected.
The problem is trying to conditionally setup the mouse events depending on the isType. While figuring out the logic, I used the # syntax to setup the mouse events #click #mousedown #mouseup etc. and everything worked great by itself. For example, the events for a momentary button during testing looked like this...
<button #mousedown="emitButtonHigh()" #mouseup="emitButtonLow" #mouseleave="emitButonLow"></button>
However, a simple toggle button looked more like this...
<button #click="emitButtonToggle()"></button>
Obviously there is a bit of conflict there.
My attempted work around was to use a switch statement in created() that would take isType as the expression and conditionally register the appropriate mouse events...
created(){
switch(this.isType){
case ("momentary"):{
//attach on events
break;
}
case ("toggle"):{
//attach on events
break;
}
case ("toggle-multi"):{
//attach on events
break;
}
default:{
break;
}
}
}
While switch itself is working, I can't figure out how to attach the mouse events in this context. I can attach a custom event no problem using...
this.$root.$on('my-custom-event', ()=>{
//do stuff
});
but trying do something like...
this.$root.$on('click', ()=>{
//do stuff
});
or...
this.$on('click', ()=>{
//do stuff
});
Does not work, nor can I figure out any way to write it out that creates the same functionality as #click let alone #mousedown #mouseup or other built-in events.
TL;DR
How do you write out the # syntax or v-on syntax, for built-in events (click, mousedown, mouseup, etc.), using $on syntax, so the events actually fire?
You could attach all these component events on a single handler, determine the event.types as they're fired and emit your custom events from here, while optionally passing additional arguments.
const SuperButton = Vue.extend({
template: `
<button
#mousedown="emitCustomEvent"
#mouseup="emitCustomEvent"
#mouseleave="emitCustomEvent">
<slot></slot>
</button>
`,
props: {
isType: {
type: String,
default:
String
},
// ...
},
methods: {
emitCustomEvent(e) {
let type = e.type.substr('mouse'.length);
let args = {
type,
isSelected: type === 'down',
args: {
// Your additional args here
}
};
switch(this.isType) {
case ("momentary"):{
//attach on events
break;
}
// ...
}
// Or emit events regardless of the "isType"
this.$emit(`mouse:${type}`, args);
this.$emit('mouse', args);
}
}
});
new Vue({
el: '#app',
methods: {
mousing(args) {
console.log(`mouse:${args.type} from component.`);
},
mouseLeaving() {
console.log('Mouse leaving.');
}
},
components: {
SuperButton
}
});
Vue.config.devtools = false;
Vue.config.productionTip = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<super-button #mouse="mousing">Super button</super-button>
<super-button #mouse:leave="mouseLeaving">Super button 2, detecting leaving only</super-button>
</div>
Fairly new to React Js so bear with me...
I'm using onClick in a child component to change the state of a parent component. The parent state is a boolean which, in turn is used to toggle a class on and off on a seperate element from that which was clicked. Something like this:
In Parent.js (relevant code only):
constructor(props) {
super(props);
this.state = {
elementOpen: false;
}
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
elementOpen: !this.state.bucketListOpen
})
}
render() {
return (
<Child
className = {this.state.elementOpen ? "open" : "closed"}
toggleClassName = {this.handleClick}
/>
)
}
In Child.js (relevant code only):
render() {
return (
<div>
<div className={this.props.className}>Test</div>
<a onClick={this.props.toggleClassName}>Click Me</a>
</div>
)
}
... and this works fine. No problems. The problem arises when I want to move the {this.props.toggleClassName} in the Child component into a handling function (the reason is so that I can fire more than one event on click). In short, it ceases to work.
I've tried creating a method in the Child component and placing the prop inside the method as follows:
handleClassSwitch() {
return this.props.toggleClassName;
}
render() {
return (
<div>
<div className={this.props.className}>Test</div>
<a onClick={this.handleClassSwitch}>Click Me</a>
</div>
)
}
...but sadly it's not working and I'm stumped. Probably really obvious but I'm struggling here. Any help appreciated.
You can call multiple functions inside handleClassSwitch, but you need to call those methods by using ().
Like this:
handleClassSwitch() {
this.props.toggleClassName(); // notice ()
function2();
function3();
}
Note: Don't forgot to bind handleClassSwitch method inside Child component, either bind inside constructor or use arrow function.
Check this snippet for the difference between these two:
function abc() {
console.log('calling abc');
return 1;
}
console.log('without () => ', abc);
console.log('with () => ', abc());
It seems that you don't actually invoke toggleClassName anymore. When you click the a tag, it calls handleClassSwitch which returns a reference to toggleClassName instead of invoking it. Try writing this instead:
handleClassSwitch = () => {
this.props.toggleClassName();
}
Notice that I use the => syntax to bind the handleClassSwitch so it has access to this
You are currently not binding your handleClassSwitch function the same way you do with handleClick in your Parent component.
It will work if you bind it in the Child component as well. You could also make handleClassSwitch into an arrow function if your would prefer.
class Child {
handleClassSwitch = () => {
this.props.toggleClassName();
};
render() {
return (
<div>
<div className={this.props.className}>Test</div>
<a onClick={this.handleClassSwitch}>Click Me</a>
</div>
);
}
}
I want to be able to intercept an elements on click event that get's triggered by a React component and then override the functionality with native javascript.
window.addEventListener('load', function() {
var el = document.getElementById('button');
el.addEventListener('click', function(e) {
e.stopPropagation();
window.location.href = 'http://www.google.com';
});
});
The above code is placed after the bundled react code and it looks as if it can't find the element? Is there a way I can wait for React to be loaded? Or is there a better way I can handle this? Or can I override/attach an event to a react element? (I cant use react as it's already bundled)
Since your React app ends up being a JS file that you reference in your index.html file, that means if you reference a node element that's generated inside of it, jQuery can't see it because it was dynamically generated.
To solve this issue you can use two React concepts: lifecycle methods, and refs.
One, create a reference of the button you're aiming to listen globally, in the parent component of that button:
class ParentComponent extends React.Component {
render() {
return (
<button ref={(button) => this.button = button}>
Click this button
</button>
);
}
}
Two, create a componentDidMount life cycle method to make use of the ref you've just created:
class ParentComponent {
componentDidMount = () => {
this.button.addEventListener('click', function (e) {
e.preventDefault();
console.log('it works?!');
});
}
render() {
return (
<button ref={(button) => this.button = button}>
Click this button
</button>
);
}
}
Keep in mind that in other lifecycle methods like componentWillMount, or componetWillUpdate refs are either not existent or are old, because the component hasn't made it's most recent render yet. So, if you want to integrate 3rd party DOM libraries, use didMount.
I actually managed to figure it out using plain javascript, here's my code:
window.addEventListener('load', function() {
var checkExist = setInterval(function() {
var el = document.getElementById('button');
if (el != null) {
clearInterval(checkExist);
el.addEventListener('click', function(e) {
e.stopPropagation();
window.location.href = 'http://www.google.com';
});
}
}, 100);
});
I am trying to learn React and came across this code on the website: https://www.reactenlightenment.com/react-state/8.2.html
I understood what the code does but could not understand what is parameter 'a' in changeMood function. I ran the code after removing it and code worked well.
var MoodComponent = React.createClass({
getInitialState: function() {
return {mood: ':|'};
},
changeMood:function(event, a){
const moods = [':)',':|',':('];
const current = moods.indexOf(event.target.textContent);
this.setState({mood: current === 2 ? moods[0] : moods[current+1]});
},
render: function() {
return (
<span style={{fontSize:'60',border:'1px solid #333',cursor:'pointer'}}
onClick={this.changeMood}>
{this.state.mood}
</span>
)
}
});
It doesn't mean anything.
The onClick only sends 1 parameter, the event, which is a Synthetic event created by React to handle cross browser compatibility.
For more info, check the React docs on handling events
I'm trying to unmount a React.js node with this._rootNodeID
handleClick: function() {
React.unmountComponentAtNode(this._rootNodeID)
}
But it returns false.
The handleClick is fired when I click on an element, and should unmount the root-node. Documentation on unmountComponentAtNode here
I've tried this as well:
React.unmountComponentAtNode($('*[data-reactid="'+this._rootNodeID+'"]')[0])
That selector works with jQuery.hide(), but not with unmounting it, while the documentation states it should be a DOMElement, like you would use for React.renderComponent
After a few more tests it turns out it works on some elements/selectors.
It somehow works with the selector: document.getElementById('maindiv'), where maindiv is an element not generated with React.js, and just plain html. Then it returns true.
But as soon as I try and select a different ElementById that is generated with React.js it returns false. And it wont work with document.body either, though they all essentially return the same thing if I console.log them (getElementsByClassName('bla')[0] also doesn't work)
There should be a simple way to select the node via this, without having to resort to jQuery or other selectors, I know it's in there somewhere..
Unmount components from the same DOM element that you mount them in. So if you did something like:
ReactDOM.render(<SampleComponent />, document.getElementById('container'));
Then you would unmount it with:
ReactDOM.unmountComponentAtNode(document.getElementById('container'));
Here is a simple JSFiddle where we mount the component and then unmount it after 3 seconds.
This worked for me. You may want to take extra precautions if findDOMNode returns null.
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);
The example I use:
unmount: function() {
var node = this.getDOMNode();
React.unmountComponentAtNode(node);
$(node).remove();
},
handleClick: function() {
this.unmount();
}
You don't need to unmount the component the simple solution it's change the state and render a empty div
const AlertMessages = React.createClass({
getInitialState() {
return {
alertVisible: true
};
},
handleAlertDismiss() {
this.setState({alertVisible: false});
},
render() {
if (this.state.alertVisible) {
return (
<Alert bsStyle="danger" onDismiss={this.handleAlertDismiss}>
<h4>Oh snap! You got an error!</h4>
</Alert>
);
}
return <div></div>
}
});
As mentioned in the GitHub issue you filed, if you want access to a component's DOM node, you can use this.getDOMNode(). However a component can not unmount itself. See Michael's answer for the correct way to do it.
First , i am new to reactjs ,too . Of course we can control the Component all by switch the state , but as I try and test , i get that , the React.unmountComponentAtNode(parentNode) can only unmount the component which is rendered by React.render(<SubComponent>,parentNode). So <SubComponent> to be removed must be appened by React.render() method , so I write the code
<script type="text/jsx">
var SubComponent = React.createClass({
render:function(){
return (
<div><h1>SubComponent to be unmouned</h1></div>
);
},
componentWillMount:function(){
console.log("componentWillMount");
},
componentDidMount:function(){
console.log("componentDidMount");
},
componentWillUnmount:function(){
console.log("componentWillUnmount");
}
});
var App = React.createClass({
unmountSubComponent:function(){
var node = React.findDOMNode(this.subCom);
var container = node.parentNode;
React.unmountComponentAtNode(container);
container.parentNode.removeChild(container)
},
componentDidMount:function(){
var el = React.findDOMNode(this)
var container = el.querySelector('.container');
this.subCom = React.render(<SubComponent/> , container);
},
render:function(){
return (
<div className="app">
<div className="container"></div>
<button onClick={this.unmountSubComponent}>Unmount</button>
</div>
)
}
});
React.render(<App/> , document.body);
</script>
Run the sample code in jsFiddle , and have a try .
Note: in the sample code React.findDOMNode is replaced by getDOMNode as the reactjs version problem .