In react.js is there any way to disable all children events - javascript

I want to build an application which has two states; pause and active. For example I want to disable all children/owned components' events like onClick, onChange, onKeyDown, .etc.
I had thought to give isActive=false prop through all it's children/owned components and check for the property isActive on event handlers. If isActive property is falsy event handler will do nothing. I can make this mechanism even easier with a simple helper function. But my concern is when I changed the app state to paused, all children components needs to be re-rendered.
Im searching for a way to bypass all event handlers (not custom ones) without re render all components.
UPDATE: I watch rendering rectangles on chrome end it doesn't re render the children. But if there any better reacty way to do this I want to learn it.

One way to solve this is using a simple guard abstraction. It works like this:
var sayHi = guard("enabled", function(){
alert("hi");
});
guard.deactivate("enabled");
sayHi(); // nothing happens
guard.activate("enabled");
sayHi(); // shows the alert
Using this for event handlers is similar:
handleChange: guard("something", function(e){
// if 'something' is deactivated, the input won't change
this.setState({value: e.target.value});
})
// or
return <div onClick={guard("something", this.handleClick)}>click me!</div>
Here's an implementation of guard
var guard = function(key, fn){
return function(){
if (guard.flags[key]) {
return fn.apply(this, arguments);
}
};
};
guard.flags = {};
guard.activate = function(key){ guard.flags[key] = true };
guard.deactivate = function(key){ guard.flags[key] = false };

Set pointerEvents='none' in the styling of the container div. It'll disable all of the children. I know it from React Native, but it seems to work in React.js as well.

Related

How to manually trigger react state update with browser javascript [duplicate]

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.
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)
}
}
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);
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.

Can I register click-listener after state transition?

I have a component that looks like this:
<div ref='carousel' onClick={this.mobileZoomOut()} className='carousel'>
The mobileZoomout is suppose to register a special condition that only applies to small screens:
mobileZoomOut () {
const elem = this.state.zoom.carousel
if (this.zoomed('carousel') && elem.scale < 1.1) {
this.setState({zoom: {}})
}
}
The regular zoom is registered like this:
this.flky = new Flickity('.carousel', flickityOptions)
this.flky.on('staticClick', (e) => {
if (this.zoomed()) {
this.setState({ zoom: {} })
} else {
this.zoomIn('carousel', 0.774, 0)
this.zoomIn('thumbs', 0.208, 0.774)
}
})
staticClick is a custom event from the image-slider flickity, it is disabled when zooming in on mobile. That is why I need another zoom-out-event on mobile.
When adding mobileZoomOut I get this error, I believe the reason is that the click event on the carousel register both events, I don't want the onClick to be registered until after the staticClick -event is done.
Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
I have tried replacing onClick with onMouseDown/onMouseUp it does not help. I think I could do it with a timeout, but I would like to avoid that and I'm not really sure how.
You are not passing your function as a prop, but rather executing it and passing its returned value.
foo() executes the foo-function. You want it executed when the event is triggered though.
this should do the trick.
<div ref='carousel' onClick={this.mobileZoomOut} className='carousel'>
you should be passing reference to function as a props.
<div ref='carousel' onClick={this.mobileZoomOut.bind(this)} className='carousel'>

ReactJs how to know when a component is removed from DOM

I'm currently facing a problem with React. I need to know when a component is removed from DOM.
All I find in component lifecycle is componentWillUnmount which gets called before the component is removed from DOM.
Is there any way to achieve this with React ?
In plain javascript ?
Thanks.
[EDIT]
#jpdelatorre
"Plain javascript" === Not using a library ;-)
My use case is the use of jsPlumb within a react component.
Basically jsPlumb is a library that draws svg in the DOM with position calculation.
In my main component there is a list of items.
Each item is a component.
On each rendered item I use JsPlumb to draw on it.
But... When I remove an item of the list the items are changing there positions in the DOM so I need to ask to jsPlumb to redraw things based on new positions. So that's why I need to know when component is fully removed from DOM.
componentWillUnmount is the correct lifecycle method. As you say, it is triggered prior to the component being removed. If you have something you need to wait to to until after it's been removed, you can use setTimeout with a short timeout value to schedule a callback once the current task completes.
componentWillUnmount is the life cycle method which can be hooked.
But as mentioned if you want to know in component1 about unmounting of component2 then you need to trigger an action in component2 in its lifecycle method, which should be subscribed in component1 and do some action in component1 listener.
As other already mentioned you need componentWillUnmount.
Here is a simple example in React(I added there some comment to get what is going one):
var Button = React.createClass({
componentWillUnmount: function(){
// console will show this message when compoent is being Unmounted("removed")
console.log('Button removed');
},
render() {
return <h1 ref='button_node'>
<ReactBootstrap.Button bsStyle="success">Red</ReactBootstrap.Button>
</h1>;
}
});
var RemoveButton = React.createClass({
getInitialState: function() {
// this state keep tracks if Button removed or not
//(you can use it for some redrawing or anything else in your code)
return {buttonMounted: true}
},
mountRedButton: function(){
ReactDOM.render(<Button/>, document.getElementById('button'));
this.setState({buttonMounted: true});
},
unmountRedButton: function(){
ReactDOM.unmountComponentAtNode(document.getElementById('button'));
this.setState({buttonMounted: false});
},
render() {
return <h1>
//based on condition if Button compoennt removed or not we show/hide different buttons
{ this.state.buttonMounted ? <ReactBootstrap.Button onClick={this.unmountRedButton } bsStyle="danger">Remove Red Button!</ReactBootstrap.Button> : null}
{ this.state.buttonMounted ? null :<ReactBootstrap.Button onClick={this.mountRedButton } bsStyle="success">Add Red Button!</ReactBootstrap.Button> }
</h1>;
}
});
// mount components
ReactDOM.render(<Button/>, document.getElementById('button'));
ReactDOM.render(<RemoveButton/>, document.getElementById('remove'));
Here is a full working example on JSFiddle
As about "plain javascript" - you are already using React JS,my example is based on React and ReactDom, nothing more(actually there is also react-bootstrap, I added it only for pretty buttons, it is not required at all)
Update:
What about use of MutationObserver? If you need a time when Node in DOM is removed but componentWillUnmount is fired before node removal(which seems unsutable for you) you can use it. Following my example with buttons:
var removalWatcher = new MutationObserver(function (e) {
var removalTimeStamp = '[' + Date.now() + '] ';
if (e[0].removedNodes.length) {
console.log('Node was removed', e[0].removedNodes, 'timestamp:', removalTimeStamp)
};
});
here is a JsFiddle with updated example. You can compare timestamps that are printed into console both for MutationObserver and React ComponentWillUnmount.
Ok thanks to all ! I found a solution that fits my needs and is (IMHO) clean...
In my parent component I handle componentDidUpdate lifecycle event and in there I can know (with prevProps and props) if the changes that are made need to fire my action...
class MyComponent extends Component {
// this one is called each time any props is changed
componentDidUpdate(prevProps, prevState) {
// if condition is matched then do something
}
}

ReactJS: How do I access my models in event handlers without binding?

Say I have a ReactJS component that represents a "Document" containing "Paragraph"s, each containing "Sentences" which I want rendered into contenteditable spans.
var paragraphData = [{
id: 1,
sentenceData: [
'Paragraph 1, Sentence 1',
'Paragraph 1, Sentence 2'
]
},{
id: 2,
sentenceData: [
'Paragraph 2, Sentence 1',
'Paragraph 2, Sentence 2'
]
}];
var Sentence = React.createClass({
render: function () {
return (<span
contenteditable="true"
onKeyDown={this.props.onKeyDown}>
{this.props.value}
</span>);
}
});
var Paragraph = React.createClass({
render: function () {
var me = this;
var sentences = this.props.sentenceData.map(function (sentenceData) {
return <Sentence value={sentenceData} onKeyDown={me.props.onKeyDown} />;
});
return <div>{sentences}</div>;
});
var Document = React.createClass({
render: function () {
var me = this;
var paragraphs = this.props.paragraphData.map(function (paragraphData) {
return <Paragraph sentences={paragraphData.sentenceData} onKeyDown={me.onKeyDown}>;
});
return <div>{paragraphs}</div>;
},
onKeyDown: function (e) {
// If "Enter" is pressed, I want to split the sentence at
// getSelection().focusOffset, update the current sentence's
// value to currentValue.substr(0, focusOffset) and insert a
// new sentence with value currentValue.substr(focusOffset),
// but how do I know which paragraph/sentences I need to
// inspect/change? Is "e.target" the only thing I have to go by?
}
});
In ReactJS, the idea is for data to flow up and events to flow down. (Which one is referred to as "up" or "down" seems to change all the time, but hopefully you know what I mean.)
My Question:
In my onKeyDown handler, how do I know which models need to have changes applied?
I thought about using .bind() to bind the handler to each model as it was passed up, but it seems a bit... wrong:
Would that be considered tight coupling between model/view?
It would mean binding hundreds or thousands of times (potentially, on a large document), each time creating a new function - which would go against the best-practice "don't create functions in a loop" principle.
I get the feeling I'm heading in the wrong direction - any help much appreciated!
It is perfectly fine to bind the onKeyDown function.
If you implement shouldComponentUpdate in Sentence, when your app is rendered again, the binded functions won't be created again because these components will not render if they have not change.
I don't think the memory overhead of 1000 or 10000 functions has too much impact, and you should not try to optimize this unless you have perf problems. What you don't want is create all these functions everytime on each render, and this is why shouldComponentUpdate is here.
It won't couple more your components that they are already (they are, because they interact together already on a well defined business context). Basically you could create a generic, uncoupled component that will receive any piece of data and on key down on the rendering of that data, will inject that data to a callback. It is generic and does not add coupling, you can control the entire behavior outside of the component.
Notice that binding functions coming from props or in loops to dom event listeners is not something forbidden by React and actually you can find exemples where it is done.
In a loop (that may have many items!): http://facebook.github.io/react/tips/communicate-between-components.html
With binded function props: (can't find it, but I do use it myself...)
Notice that in React, it is forbidden to rebind a function of a component because React bind all functions to its component.
So if a function is in Document and is injected to Sentence, you can't bind it to this in Sentence because it does not make sens and is forbidden by React.
This is the code that does it: https://github.com/facebook/react/blob/95de877dceeaac08755cfe1142a853c467d91d58/src/core/ReactCompositeComponent.js#L1291
if (newThis !== component && newThis !== null) {
monitorCodeUse('react_bind_warning', { component: componentName });
console.warn(
'bind(): React component methods may only be bound to the ' +
'component instance. See ' + componentName
);
}
Note that a newThis !== null has been added. This was actually added to fix this problem when trying to bind props functions.
Now you can write
<Sentence value={sentenceData} onKeyDown={me.props.onKeyDown.bind(null,sentenceData} />
And on document you have a listener:
onKeyDown: function (sentenceData) { }
This is perfectly fine :)
Notice that there is another way this problem can be solved which may be a little bit more efficient:
var Sentence = React.createClass({
onKeyDown: function(e) {
this.props.onKeyDown(this.props.value)
},
render: function () {
return (<span
contenteditable="true"
onKeyDown={this.props.onKeyDown}>
{this.props.value}
</span>);
}
});
Instead of binding a function, you simply create a class that has this unique function that you would normally bind. In this case I think however it creates more coupling as you don't really control the behavior of the callback from outside the Sentence component.
You're concerning yourself with the wrong problem.
Wherever the relevant onKeyDown handler that you're referring to should be working (i.e. for sentences, or paragraphs), you need to handle the data change and then call:
this.setState({stateName: newdata})
This will cause react to re-render the component with the new data.
For a good example, look at facebook's tutorial for a comment widget.
You can see how the form handles the data change by adding new comments, and then calls setState() to re-render with the additional comments. React handles everything else.
In your example, if you change something within a paragraph, the paragraph should call setState with the new sentence data after you've done all the work you need, and then react will handle the rest.

Apply actions on Javascript events depending on current view context in AngularJS

I am building an AngularJS SPA which has multiple elements that I want to bind keypress events to. The difficulty comes in the fact that, due to the Pinterest style modal window routing, several of the elements, and thus controller scopes, can be visible at once. This means that certain keypress event will fire in more than one controller, including potential background content.
What I want is to bind the events, or at least only take action on those events depending on the current state / context of the application. Is there a good way to handle this?
What I have considered:
Using a Service that maintains a reference to the state and handles the event binding on a global level to which controllers can subscribe to events based on their context. But I do not know how to subscribe functions to an Angular service in this manor.
Unbind and bind events on the $routeChange event but feel this could get very messy.
I know this is a conceptual question but I have been stuck on this for some time now and any help would be much appreciated.
Update
I have attempted to make a Plunk here to demonstrate this. Each context (an abstract state of the application) has a directive that binds to a keypress event. I want event handler on the context in view (i.e. the active state) to be the only one that executes.
I have tried to make a simplified but relevant example. Note:
Most but not all of the contexts/states will have a route associated so I cant just rely on $stateChange events
Many states are modal windows, meaning background elements still visible may also be listening for a key press. I am not sure I can guarantee the DOM order in all cases.
I have tried using the elements focus, but this does not work (think tabbing out and back into the application, problems when those elements involve forms etc.)
Hope this makes it clearer, I am happy to update further.
After a lengthy discussion in comments (see above), it boiled down to the requirement of a service that keeps track of the current state and key-presses and accepts "subscriptions" for key-presses when in a specific state.
Below is a generic implementation of such a service, with the following attributes:
The current-state is set externally (you can implement it is any way you like: ngRoute, uiRouter, $location change events or whatever else might determine the state of your app).
It exposes two methods: One for setting the state and one for subscribing to a key-down event for a specific state and keyCode.
The subscribe function returns an unsubscribe function which can be used for...you guessed it...unsubscribing !
This demo implementation assumes that at the time of triggering the key-down event there is no $digest cycle in progress. If you have different requirements, you can check before calling $rootScope.$apply().
app.factory('StateService', function ($document, $rootScope) {
var currentState = 'init';
var stateCallbacks = {};
function fireKeydown(evt) {
$rootScope.$apply(function () {
((stateCallbacks[currentState] || {})[evt.keyCode] || []).forEach(
function (cb) {
cb(evt, currentState);
}
);
});
}
function setState(newState) {
currentState = newState;
}
function subscribeToKeydown(state, keyCode, callback) {
if (!state || !keyCode || !callback) {
return angular.noop;
}
stateCallbacks[state] = stateCallbacks[state] || {};
stateCallbacks[state][keyCode] = stateCallbacks[state][keyCode] || [];
stateCallbacks[state][keyCode].push(callback);
function unsubscribe() {
return ((stateCallbacks[state] || {})[keyCode] || []).some(
function (cb, idx, arr) {
if (cb === callback) {
arr.splice(idx, 1);
console.log('Unsubscribed from state: ' + state);
return true;
}
}
);
}
console.log('Subscribed to state: ' + state);
return unsubscribe;
}
$document.on('keydown', fireKeydown);
return {
setState: setState,
subscribeToKeydown: subscribeToKeydown
};
});
See, also, this short demo.

Categories

Resources