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);
});
Related
I`m building third party application for specific sites with Jquery.
Recently I started to use rx.Observable in my project. However, I found to use of this new JS library sometimes is hard to understand. I have tried to convert next peace of code to use with Observables, but it is not working at all;
class EventsUtils {
constructor() {
this.observable = Rx.Observable;
}
bindUserLeavePageEvent() {
var self = this;
document.addEventListener('mouseleave', (e) => {
$JQ(document).trigger('mouseleave.mo');
}, false);
/*We cannot remove document mouse over event thus we trigger Jquery registered custom event and on remove we cancel it*/
$JQ(document).off('mouseleave.mo').on('mouseleave.mo', (e) => {
if (e.clientY < 0 && !self.loaded) {
console.log('loading from screen Leave');
$JQ('.fixed-button').trigger('click');
self.loaded = true;
}
});
}
$JQ variable is came from jquery.noConflict due to i am running not on my page.
To convert second expression to Observable I have tried to use next statement:
this.observable.fromEvent(document, 'mouseleave.mo').pluck('currentTarget').subscribe(x=>console.log(x));
}
But without success.
How to convert above event statements to use with Observable and what is common pattern to do this;
It seems as if jquery.trigger does not really work with custom events - you can only catch those events through $(elem).on as they are handles internally for browser-compatibility-reasons. (https://github.com/jquery/jquery/issues/2476)
But you can relatively easy dispatch custom events (unless you want to target IE<=8)
document.addEventListener("mouseleave", () => {
console.log("Original event: Leave");
// dispatching custom events with vanilla-js (should work all the way down to IE9)
const event = document.createEvent("CustomEvent");
event.initEvent("mo.leave", true, true);
document.dispatchEvent(event);
});
Rx.Observable
.fromEvent(document, "mo.leave")
.pluck("currentTarget")
.subscribe(target => console.info("Target is", target.nodeName));
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
I am using $.observable(array).insert() to append items to a list. This is updating my view as it should: new list items are rendered to the DOM. However, I would like to issue a click event on the new DOM node (I'm relying on the event to add a class to expand the item and attach another listener to the body so the area can be closed).
I have tried both
$.observable(_model.leadTimes).insert(leadTime);
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
...and
function watchLeadTimes() {
var changeHandler = function (ev, eventArgs) {
if (eventArgs.change === 'insert') {
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
}
};
$.observe(_model.leadTimes, changeHandler);
}
And neither of them worked, however, if I wrap the jQuery method in a setTimout, like setTimeout(function () { $leadTimes.find('.lead-time-data').last().find('.start-editing').click(); }, 400);, it does work, leading me to believe this is an issue of timing with the DOM render somehow not finishing before my jQuery click() method is invoked.
Since the odds are decent that you will see this, Borris, thank you for the library and all that you do! I think jsViews is an excellent middle ground between the monolithic frameworks out there and plain old jQuery noodling!
Edit 02/09/17
It turns out my issue was overlapping click events--I was inadvertently handling a click to deselect my element immediately after it was selected. However I took the opportunity to rewrite things to use a more declarative approach following Borris' linked example.
Now in my template I am using a computed observable, isSelected to toggle the .editing class:
{^{for leadTimes}}
<tr class="lead-time-data" data-link="class{merge:~isSelected() toggle='editing'}">
<span>{^{:daysLead}}</span>
</tr>
{{/for}}
And this JS:
function addNewLeadTimeClickHandler() {
var onNewLeadTimeClick = function () {
e.stopPropagation(); // this is what I was missing
var leadTime = {
daysLead: 1,
description: ''
};
$.observable(_model.activityMapping.leadTimes).insert(leadTime);
selectLeadtime(_model.activityMapping.leadTimes.length -1);
}
$leadTimes.on('click', '.add', onNewLeadTimeClick);
}
function selectLeadtime(index) {
var addStopEditingClickHandler = function () {
var onClickHandler = function (event) {
if ($(event.target).closest('tr').hasClass('editing')) {
setHandler();
return;
}
selectLeadtime(-1)
};
function setHandler() {
var clickEvent = 'click.ActivityChangeRequestDetailController-outside-edit-row';
$('html:not(.edit)').off(clickEvent).one(clickEvent, onClickHandler);
};
setHandler();
}
if (_model.selectedLeadtimeIndex !== index) {
$.observable(_model).setProperty('selectedLeadtimeIndex', index)
addStopEditingClickHandler();
}
}
function isSelected() {
var view = this;
return this.index === _model.selectedLeadtimeIndex;
}
// isSelected.depends = ["_model^selectedLeadtimeIndex"];
// for some reason I could not get the above .depends syntax to work
// ...or "_model.selectedLeadtimeIndex" or "_model.selectedLeadtimeIndex"
// but this worked ...
isSelected.depends = function() {return [_model, "selectedLeadtimeIndex"]};
The observable insert() method is synchronous. If your list items are rendered simply using {^{for}}, then that is also synchronous, so you should not need to use setTimeout, or a callback. (There are such callbacks available, but you should not need them for this scenario.)
See for example http://www.jsviews.com/#samples/editable/tags (code here):
$.observable(movies).insert({...});
// Set selection on the added item
app.select($.view(".movies tr:last").index);
The selection is getting added, synchronously, on the newly inserted item.
Do you have other asynchronous code somewhere in your rendering?
BTW generally you don't need to add new click handlers to added elements, if you use the delegate pattern. For example, in the same sample, a click handler to remove a movie is added initially to the container "#movieList" with a delegate selector ".removeMovie" (See code). That will work even for movies added later.
The same scenario works using {{on}} See http://www.jsviews.com/#link-events: "The selector argument can target elements that are added later"
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?
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 .
I am new to Jasmine and seem to be struggling to get what I think is a fairy standard kind of thing running.
I am loading an HTML file via a fixture and trying to call a click on an element on the dom. This I would expect result in the call to the method of the JS file I am trying to test. When I try and debug this in developer tools the method that should be called in my js file never hits a breakpoint. As such I assume that code is not being called and therfore does not toggle the expand/collapse class.
My test:
describe("userExpand", function () {
beforeEach(function () {
loadFixtures('user-expand.html');
//userControl();
//this.addMatchers({
// toHaveClass: function (className) {
// return this.actual.hasClass(className);
// }
//});
});
//this test works ok
it("checks the click is firing", function () {
spyOnEvent($('.expanded'), 'click');
$('.expanded').trigger('click');
expect("click").toHaveBeenTriggeredOn($('.expanded'));
});
//this doesn't
it("checks the click is changing the class", function () {
//spyOnEvent($('.collapsed'), 'click');
var myElement = $('.collapsed');
myElement.click();
expect(myElement).toHaveClass('.expanded');
});
Part of the fixture:
<div class="wrapper">
<div class="row group">
<div class="col-md-1" data-bordercolour=""> </div>
<div class="collapsed col-md-1"> </div>
<div class="col-md-9">None (1)</div>
The JS I am trying to test:
var userControl = function () {
"use strict";
var collapse = '.collapsed';
var expand = '.expanded';
var userList = $(".userList");
function toggleState() {
var currentControl = $(this);
if (currentControl.hasClass('all')) {
if (currentControl.hasClass('expanded')) {
toggleIcon(currentControl, collapse);
userList.find(".user-group-summary").hide()
.end()
.find(".user-group-info").show();
} else {
toggleIcon(currentControl, expand);
userList.find(".user-group-summary").show()
.end()
.find(".user-group-info").hide();
}
} else {
currentControl.parent().nextUntil('.group').toggle();
currentControl.toggleClass("expanded collapsed");
currentControl.parent().find(".user-group-summary").toggle()
.end()
.find(".user-group-info").toggle();
}
};
function toggleIcon(ctrl, currentState) {
var details = ctrl.closest('div.row').siblings('.wrapper');
details.find(currentState).toggleClass('expanded collapsed');
if (currentState === expand) {
details.find('.detail').hide();
} else {
details.find('.detail').show();
}
}
userList.on('click', '.expanded, .collapsed', toggleState);
$('[data-bordercolour]').each(function () {
$(this).css("background-color", $(this).data('bordercolour'))
.parent().nextUntil('.group')
.find('>:first-child').css("background-color", $(this).data('bordercolour'));
});
return {
toggleState: toggleState
};
}();
The code works fine in normal use so I am sure I am missing something obvious with the way Jasmine should be used. Any help would be appreciated.
Update:
I can make the togglestate method fire by using call in the test rather than triggering a click event:
it('checks on click of icon toggles that icon', function () {
var myElement = $('.collapsed');
userControl.toggleState.call(myElement);
expect(myElement).toHaveClass('expanded');
});
This seems a little strange as all the examples I can find are quite happy with click. Gets me off the hook but I would still like to know what I am missing.
It's hard to give a precise hint without the source code. Does click on .collapsed involve asynchronous action(s)? If so, wrapping the test in runs(...); waitsFor(...); runs(...); may solve the problem. Check the Jasmine introduction for how to do this.