Is there a Reacty way to create a wrapper component of another element that has bound DOM events, without producing another element (as in <div>)?
E.g.
class TripleClickWrapper extends Component {
render() {
return <div onClick={::this._onClick}>{this.props.children}</div>
}
_onClick() { /* counts clicks and handles timeouts etc */ }
}
// somewhere else:
<TripleClickWrapper onTripleClick={::this._doSomething}>
<SomeComponent />
</TripleClickWrapper>
I don't want the extra <div> TripleClickWrapper creates, but I want to bind onClick to the wrapper, without passing it down to <SomeComponent>. Any nice way without getting to DOMy (findDOMNode+addEventLisetener+remove on unmout)?
If I didn't need to bind DOM events, I could just return React.Children.only(this.props.children).
You can return an augmented/cloned version of the child component in your render function: https://facebook.github.io/react/docs/top-level-api.html#react.cloneelement
You can add additional props during the cloning process.
render() {
const newProps = /* any props/event handlers you want to add */;
return React.cloneElement(React.Children.only(this.props.children), newProps);
}
Based on the source code (specifically, this line: https://github.com/facebook/react/blob/v15.0.0-rc.2/src/renderers/dom/shared/ReactDOMComponent.js#L621 ) I see that a possible solution might be to inject an EventPlugin. This is because enqueuePutListener is called, which in turn calls ReactBrowserEventEmitter.listenTo --> ReactEventListener.trapBubbledEvent --> EventListener.listen which does the actual addEventListener. The if statement there looks for a plugin name. I'm not sure how that mechanism works, and didn't find it in the docs, but with some investigation sounds like a possible solution. For example, there's a plugin that handled tap events when 300ms delay was still a thing.
I ended up with this approach:
I didn't want an extra <div>
I didn't want my wrapper to override the child's events (so if my wrapped div already had onClick it should still run
I didn't want to use addEventListener manually on the findDOMNode(this). Reasons: 1) inner onClicks can't run e.stopPropagation() and stop my added event, as React events run on the document, without useCapture, and 2) lose React event system goodies.
Currently I'm taking the only child in the wrapper and use cloneElement, as #Calvin Belden recommended, and pass in my events combined with existing events.
render() {
const onlyChild = React.Children.only(children)
const events = {
onClick: this.doSomething,
onMouseEnter: this.doSomethingElse,
// ...
}
for (let [eventName, fn] of Object.entries(events)) {
if (onlyChild.props[eventName]) {
let oldFn = onlyChild.props[eventName]
events[eventName] = e => {
oldFn(e)
if (!e.isPropagationStopped()) fn(e)
}
}
}
return React.cloneElement(onlyChild, events)
}
It still forces me to pass in events when I use a component instead of a div: <TripleClickWrapper><SomeComponent></TripleClickWrapper>. SomeComponent would have to pass down onClick to its div.
Not optimal, but as clean as it can be.
We use Backbone + ReactJS bundle to build a client-side app.
Heavily relying on notorious valueLink we propagate values directly to the model via own wrapper that supports ReactJS interface for two way binding.
Now we faced the problem:
We have jquery.mask.js plugin which formats input value programmatically thus it doesn't fire React events. All this leads to situation when model receives unformatted values from user input and misses formatted ones from plugin.
It seems that React has plenty of event handling strategies depending on browser. Is there any common way to trigger change event for particular DOM element so that React will hear it?
For React 16 and React >=15.6
Setter .value= is not working as we wanted because React library overrides input value setter but we can call the function directly on the input as context.
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, 'react 16 value');
var ev2 = new Event('input', { bubbles: true});
input.dispatchEvent(ev2);
For textarea element you should use prototype of HTMLTextAreaElement class.
New codepen example.
All credits to this contributor and his solution
Outdated answer only for React <=15.5
With react-dom ^15.6.0 you can use simulated flag on the event object for the event to pass through
var ev = new Event('input', { bubbles: true});
ev.simulated = true;
element.value = 'Something new';
element.dispatchEvent(ev);
I made a codepen with an example
To understand why new flag is needed I found this comment very helpful:
The input logic in React now dedupe's change events so they don't fire
more than once per value. It listens for both browser onChange/onInput
events as well as sets on the DOM node value prop (when you update the
value via javascript). This has the side effect of meaning that if you
update the input's value manually input.value = 'foo' then dispatch a
ChangeEvent with { target: input } React will register both the set
and the event, see it's value is still `'foo', consider it a duplicate
event and swallow it.
This works fine in normal cases because a "real" browser initiated
event doesn't trigger sets on the element.value. You can bail out of
this logic secretly by tagging the event you trigger with a simulated
flag and react will always fire the event.
https://github.com/jquense/react/blob/9a93af4411a8e880bbc05392ccf2b195c97502d1/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js#L128
At least on text inputs, it appears that onChange is listening for input events:
var event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
Expanding on the answer from Grin/Dan Abramov, this works across multiple input types. Tested in React >= 15.5
const inputTypes = [
window.HTMLInputElement,
window.HTMLSelectElement,
window.HTMLTextAreaElement,
];
export const triggerInputChange = (node, value = '') => {
// only process the change on elements we know have a value setter in their constructor
if ( inputTypes.indexOf(node.__proto__.constructor) >-1 ) {
const setValue = Object.getOwnPropertyDescriptor(node.__proto__, 'value').set;
const event = new Event('input', { bubbles: true });
setValue.call(node, value);
node.dispatchEvent(event);
}
};
I know this answer comes a little late but I recently faced a similar problem. I wanted to trigger an event on a nested component. I had a list with radio and check box type widgets (they were divs that behaved like checkboxes and/or radio buttons) and in some other place in the application, if someone closed a toolbox, I needed to uncheck one.
I found a pretty simple solution, not sure if this is best practice but it works.
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': false
});
var node = document.getElementById('nodeMyComponentsEventIsConnectedTo');
node.dispatchEvent(event);
This triggered the click event on the domNode and my handler attached via react was indeed called so it behaves like I would expect if someone clicked on the element. I have not tested onChange but it should work, and not sure how this will fair in really old versions of IE but I believe the MouseEvent is supported in at least IE9 and up.
I eventually moved away from this for my particular use case because my component was very small (only a part of my application used react since i'm still learning it) and I could achieve the same thing another way without getting references to dom nodes.
UPDATE:
As others have stated in the comments, it is better to use this.refs.refname to get a reference to a dom node. In this case, refname is the ref you attached to your component via <MyComponent ref='refname' />.
You can simulate events using ReactTestUtils but that's designed for unit testing.
I'd recommend not using valueLink for this case and simply listening to change events fired by the plugin and updating the input's state in response. The two-way binding utils more as a demo than anything else; they're included in addons only to emphasize the fact that pure two-way binding isn't appropriate for most applications and that you usually need more application logic to describe the interactions in your app.
For HTMLSelectElement, i.e. <select>
var element = document.getElementById("element-id");
var trigger = Object.getOwnPropertyDescriptor(
window.HTMLSelectElement.prototype,
"value"
).set;
trigger.call(element, 4); // 4 is the select option's value we want to set
var event = new Event("change", { bubbles: true });
element.dispatchEvent(event);
I stumbled upon the same issue today. While there is default support for the 'click', 'focus', 'blur' events out of the box in JavaScript, other useful events such as 'change', 'input' are not implemented (yet).
I came up with this generic solution and refactored the code based on the accepted answers.
export const triggerNativeEventFor = (elm, { event, ...valueObj }) => {
if (!(elm instanceof Element)) {
throw new Error(`Expected an Element but received ${elm} instead!`);
}
const [prop, value] = Object.entries(valueObj)[0] ?? [];
const desc = Object.getOwnPropertyDescriptor(elm.__proto__, prop);
desc?.set?.call(elm, value);
elm.dispatchEvent(new Event(event, { bubbles: true }));
};
How does it work?
triggerNativeEventFor(inputRef.current, { event: 'input', value: '' });
Any 2nd property you pass after the 'event' key-value pair, it will be taken into account and the rest will be ignored/discarded.
This is purposedfully written like this in order not to clutter arguments definition of the helper function.
The reason as to why not default to get descriptor for 'value' only is that for instance, if you have a native checkbox <input type="checkbox" />, than it doesn't have a value rather a 'checked' prop/attribute. Then you can pass your desired check state as follows:
triggerNativeEventFor(checkBoxRef.current, { event: 'input', checked: false });
I found this on React's Github issues: Works like a charm (v15.6.2)
Here is how I implemented to a Text input:
changeInputValue = newValue => {
const e = new Event('input', { bubbles: true })
const input = document.querySelector('input[name=' + this.props.name + ']')
console.log('input', input)
this.setNativeValue(input, newValue)
input.dispatchEvent(e)
}
setNativeValue (element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set
const prototype = Object.getPrototypeOf(element)
const prototypeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
'value'
).set
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value)
} else {
valueSetter.call(element, value)
}
}
Triggering change events on arbitrary elements creates dependencies between components which are hard to reason about. It's better to stick with React's one-way data flow.
There is no simple snippet to trigger React's change event. The logic is implemented in ChangeEventPlugin.js and there are different code branches for different input types and browsers. Moreover, the implementation details vary across versions of React.
I have built react-trigger-change that does the thing, but it is intended to be used for testing, not as a production dependency:
let node;
ReactDOM.render(
<input
onChange={() => console.log('changed')}
ref={(input) => { node = input; }}
/>,
mountNode
);
reactTriggerChange(node); // 'changed' is logged
CodePen
well since we use functions to handle an onchange event, we can do it like this:
class Form extends Component {
constructor(props) {
super(props);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.state = { password: '' }
}
aForceChange() {
// something happened and a passwordChange
// needs to be triggered!!
// simple, just call the onChange handler
this.handlePasswordChange('my password');
}
handlePasswordChange(value) {
// do something
}
render() {
return (
<input type="text" value={this.state.password} onChange={changeEvent => this.handlePasswordChange(changeEvent.target.value)} />
);
}
}
The Event type input did not work for me on <select> but changing it to change works
useEffect(() => {
var event = new Event('change', { bubbles: true });
selectRef.current.dispatchEvent(event); // ref to the select control
}, [props.items]);
This ugly solution is what worked for me:
let ev = new CustomEvent('change', { bubbles: true });
Object.defineProperty(ev, 'target', {writable: false, value: inpt });
Object.defineProperty(ev, 'currentTarget', {writable: false, value: inpt });
const rHandle = Object.keys(inpt).find(k => k.startsWith("__reactEventHandlers"))
inpt[rHandle].onChange(ev);
A working solution can depend a bit on the implementation of the onChange function you're trying to trigger. Something that worked for me was to reach into the react props attached to the DOM element and call the function directly.
I created a helper function to grab the react props since they're suffixed with a hash like .__reactProps$fdb7odfwyz
It's probably not the most robust but it's good to know it's an option.
function getReactProps(el) {
const keys = Object.keys(el);
const propKey = keys.find(key => key.includes('reactProps'));
return el[propKey];
}
const el = document.querySelector('XX');
getReactProps(el).onChange({ target: { value: id } });
Since the onChange function was only using target.value I could pass a simple object to onChange to trigger my change.
This method can also help with stubborn react owned DOM elements that are listing for onMouseDown and do not respond to .click() like you'd expect.
getReactProps(el).onMouseDown(new Event('click'));
If you are using Backbone and React, I'd recommend one of the following,
Backbone.React.Component
react.backbone
They both help integrate Backbone models and collections with React views. You can use Backbone events just like you do with Backbone views. I've dabbled in both and didn't see much of a difference except one is a mixin and the other changes React.createClass to React.createBackboneClass.
I am using Marionette's CollectionView to render a list of items with ItemViews. Whenever a new item is added, I want to run a short fade-in animation. But not when the collection is rendered initially (or the collection is reset).
Before using Marionette, I handled the reset and add events slightly differently, but I can not figure out how to do this here. I looked at the source code and it seems that addItemView is responsible for adding the child view and both addChildView (called when add is triggered on the collection) and render (for reset events) call this method.
Maybe I am missing something obvious.
This is one way of doing it:
Include these functions in your CompositeView declaration:
onBeforeRender: function(){
this.onBeforeItemAdded = function(){};
},
onRender: function(){
this.onBeforeItemAdded = myAnimation;
}
This is similar to the solution I present in my book on Marionette (https://leanpub.com/marionette-gentle-introduction/)
How it works: Marionette triggers the "before:render" before it renders the entire collection, so you can set the the onBeforeItemAdded function to do nothing. Once the collection has been rendered, set that function to animate the new item view.
Since each time the collection view adds an item view it also triggers the "before:item:added", you can define an onBeforeItemAdded function that will automatically be called when that event is triggered. This matching happens thanks to triggerMethod.
This solution should solve your problem, without your needing to add flags on the model.
David Sulc answer is pretty hacky, fadeIn should be defined within item it self, not within parent view.
Another thing is that onBeforeItemAdded() is not mentioned in documentation, so it could be for internal use and may change over time.
What I suggest is to add following to parent view, note flag parentRendered:
itemViewOptions: function() {
return {
collection: this.collection,
parentRendered: this.rendered
};
},
onRender: function() {
this.rendered = true;
}
and use that flag in onShow function inside item view:
onShow: function() {
// show visual effect on newly added items
if (this.options.parentRendered) {
this.$el.css('opacity', 0).slideDown(200).animate(
{ opacity: 1 },
{ queue: false, duration: 400 }
);
}
else {
this.$el.show();
}
}
I think that your best choice is to bind your event after the CollectionView has been rendered.
myCollectionView.on( "render", function() {
this.on( "after:item:added", executeMyAnimation );
});
I have a datepicker that sets a date on an input. I am binding it with stickit. The problem is that since the datepicker (and not a keystroke) changes the value of the input, the stickit binding doesn't observe the change. If I enter the date manually, there is no problem.
bindings: {
'input[name=RecordDate]': {
observe: 'recdate',
onSet: 'dosome'
}
},
I would recommend adding a global handler to handle all of the datepickers across your project. The following handler will match any bound element with the class: 'jquery-datepicker':
Backbone.Stickit.addHandler({
selector: '.jquery-datepicker',
initialize: function($el, model, options) {
$el.datepicker({
onChangeMonthYear: function() {
model.set(options.observe, $el.val());
}
});
}
});
Here is an example fiddle:
http://jsfiddle.net/px6UP/29/
More about handlers.
Let me know if that works for you. I plan on documenting handlers better with a cookbook or examples in the near future...
events: {
'click .pdf_preview' : 'onPreviewPdfClick',
'click a.next' : '_nextPointer',
'click a.prev' : '_prevPointer',
'mouseleave .mediapop-item-container' : 'onMouseLeave',
'mouseenter .mediapop-item-container' : 'onMouseEnter',
// Editable events
'click &.editable .dialog-header h3' : 'showEditTitleForm' <--- this
},
I create a popup view that has an edit mode. While I could simply extend this view with an editable version, what I need is very basic, so instead I set it up so if you pass this.options.editable the view.el element will have an "editable" class on it.
I am wondering if there is a way for me to specify that selector inside of the event object. That way it won't event trigger if this.options.editable is not true. If Backbone supported a SASS style selector syntax then the above would work. The only alternative is to check for this.$el.hasClass('ediable') within showEditTitleForm.
I know if you set a selector like this:
'click' : 'myAction',
The click event will be applied to the this.el container. I'm looking to only apply the callback if the additional class is there.
Is it possible to do something similar to the above without adding a check within the callback method?
Edit: typo
You can always modify events before delegateEvents is called, just be careful not to modify the events attached to the view's prototype. Something like this:
initialize: function() {
if(this.options.editable) {
this.events = _({}).extend(this.events, {
'click .dialog-header h3': 'showEditTitleForm'
});
}
}
Note the _({}).extend(...), that will merge this.events (which comes from the prototype) and the extra event into an empty object so that you don't accidentally alter the events in the prototype.
Demo: http://jsfiddle.net/ambiguous/dNYXN/