How can I abstract multiple onChange events through a single React Function - javascript

I'm building a simple inventory system that uses bi-directional data flow to render a form from state, the form can then be updated by the user and changes feed back into state. So far so good. Now I'd like to simplify my code as I have a bunch of different onChange Handlers -as each form input has a unique handler. How can I simplify my code and have a single onChange handler? I tried passing the state attribute that I wish to edit through the callBacks - but this doesn't work. However if I explicitly specify the state attribute I wish to edit then it works fine.
The top example works but the bottom example doesn't. What am I missing here?
MethodUpdateInventoryState (key, event, updatedValue) {
var fishState = this.state.fishes;
var eventTarget = "name";
fishState[key].name = updatedValue;
this.setState({fishes: fishState});
}
MethodUpdateInventoryState (key, event, updatedValue) {
var fishState = this.state.fishes;
var eventTarget = "name";
fishState[key].eventTarget = updatedValue;
this.setState({fishes: fishState});
}
Gracias!

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.

Trigger onChange when setting State in componentDidMount?

I need to add some query paramaters to my url as a person checks off checkboxes.
I am using react router so I do something like this in my checkboxes on change event.
const stringified = queryString.stringify(parsed);
const path = `${this.props.location.pathname}?${stringified}`;
this.props.history.replace(path)
This does however seem to cause a re-render of the page(not sure if this should be happening, would prefer it not to do that so maybe I got to use something other than replace?).
I wanted to check on componentDidMount the url to see if the value is there. If it is there then I wanted to update the state of the checkbox.
#observable
isChecked = false;
#action
componentDidMount() {
const parsed = queryString.parse(this.props.location.search);
this.isChecked = parsed && parsed["param"] === this.props.option;
}
However I don't see the onChange being trigger.
Right now I have on change a function that takes the value and uses it to filter, so I need the function to run.
I could put that function in the componentDidMount but I wanted to make sure before I do that, there is nothing I am missing on why the change event is not be fired.
Try setting the state in the constructor() and incomponentDidUpdate().
When a URL parameter is added to the same route, the existing component is utilized (i.e. an update event) vs. a new one being created. As a result, you won't see a componentDidMount() event.
Another option/solution is to update the state of isChecked directly and push the history/url change.
If what you are trying to prevent is the page refresh use this built in function in your onSubmit event(if I understand your question correctly.)
event.preventDefault();
It stops the browser from auto-refreshing! Make sure to call event in your function though.
ie
onSubmit=(event)=>{
event.preventDefault();
//rest of code
}
If you are trying to filter, the es6 .filter method is useful for checkboxes. I personally used a select dropdown menu to filter the options and selectively show the ticked items in a ToDo List: "Done" "Active" "Completed" and used those states in my filter method.

React - setting input value with JavaScript does not trigger 'onChange'

In my React app (version 15.4.2), I am updating the value of a text input field with JavaScript - however, I have an onChange event listener associated with the input field, and changing the value of the input field does not trigger the handler (good old fashioned typing in the input field does, of course), though the content of the field is correctly updated.
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(event){
let attribute = event.target.name;
let updatedGroup = this.state.group;
updatedGroup[attribute] = event.target.value;
this.setState({group: updatedGroup});
}
addMember(memberId) {
let inputField = document.getElementById("members");
let inputValues = inputField.value.split(",");
inputField.value = [...inputValues, memberId];
}
render(){
<input type="text" id="members" name="members" value={this.state.group.members} onChange={this.onChange} />
}
So when addMember() is called (via button click in a child component), then content of the input field itself is correctly updated, but onChange is not called and thus the state is not updated, etc...
Is there a way that I can programatically set the value of the input field and trigger onChange?
What I always do in this situation is have your onChange event handler be a function that passes the event data (the character that was entered or the aggregate string) into another function. I put all of the business logic in that function. That way, if I want to invoke the business logic, I just call that method.
Since you are asking "Is there a way that I can programatically set the value of the input field and trigger onChange?" Why not skip onChange and call the business logic function from your function that is programmatically setting the value?
Onchange method will get triggered only when you type something, if you use document.getElementById and replace its values, it will directly replace the value in DOM, onChange will not get triggered in that case. Since you are using the react, I think you should avoid the direct DOM manipulation.
You are using controlled input, so in addMember method instead of updating the value in DOM, update the state value.
Try this addmember method:
addMember(memberId) {
//let inputField = document.getElementById("members");
//let inputValues = inputField.value.split(",");
//inputField.value = [...inputValues, memberId];
let group = this.state.group.slice();
group[members] = group[members] + ',' + memberId;
this.setState({
group
});
}

Centralized function to delegate form onSubmit to Flux Actions

I have a react app with many different forms I'm looking to not have to write the same code over and over for each form, and I'm looking for a simple way to centralize the onSubmit() handlers for all forms. From a central location I can trigger the specific Flux action that will trigger an ajax call.
I'm picturing one function that takes care of all form requests.
function (event) {
event.preventDefault()
var $elm = $(event.target)
var d = {}
d.method = $elm.attr('method')
d.action = $elm.attr('action')
d.data = $elm.serialize()
console.log(d)
}
From here I can check the action and method and make a switch for each possibility.
Does this make sense within the Flux architecture?
You will anyway need to collect data from specific components, so you will have to write individual functions for it, but the rest of task like sending an action can be done in the onSubmit function of a parent component. So delegate all onSubmit events in children components with data to the onSubmit functions of parent component via props.
You can also add an identifier like we do as constants in dispatchers, to identify particular onSubmit event and then in parent component use switch statements to send particular actions based on identifier.
If there's a use case, I guess it makes sense.

How can I access child components values from a parent component when they are added dynamically?

Current Working Example
I am creating a search form that has a varying number of input elements based on the users selection from a select box.
I have broken this up into three components, a wrapper called SearchContainer, a select box called SearchSelect, and the inputs within components called SearchWithTwo and SearchWithOne just for the sake of the example.
App
└─SearchContainer Form
│ SearchSelect
│ ... any one of the multiple search inputs (SearchWithOne, SearchWithTwo)
When a user changes the value of the select box the related component which contains the inputs is loaded. The component could have anywhere from one to ten inputs. All the examples I've seen mention using ref which would be great if my inputs weren't changing.
I currently have it working by using the following in the onSubmit handler for SearchContainer
handleSubmit: function(e) {
e.preventDefault();
var form = this.getDOMNode();
[].forEach.call(form.elements, function(e){
// get the values
});
// submit the values to get results.
}
However this doesn't feel like the proper way to be doing this. Is there a better recommended way to iterate through the children components and read their state? Or can I somehow pass the children into the parent state and get the values that way?
I think I have a solution in the form of a fork of your fiddle, and I'll cover the main ideas below.
First, I'm no React expert, but I like the idea of it, and I know it's gaining popularity so I want to learn more. What I don't know is the right way to use composition or inheritance to reduce the code duplication shown below.
Basically, my idea is to add a method to each search class that exposes its state to calling classes. This is implemented here as a very simple function inside the createClass call:
getData: function() {
return this.state;
},
It's so simple, there has to be a way to create a base class or mixin class with this method and then inherit/compose over it with other classes. I'm just not sure how. For now, you can just copy-paste these lines wherever it makes sense to expose a component's state.
Keep in mind, this is anathema to the Flux architecture everyone loves. In Flux, state always comes from a source object which lives outside the React components.
Anyway, abandoning larger architecture concerns for now, you can just grab that state variable by calling getData in the handleSubmit method. No DOM traversal required:
handleSubmit: function(e) {
e.preventDefault();
var form = this.getDOMNode(),
fd = new FormData(form);
var submitData = this.state.SearchBox.getData();
// submit the values to get results.
},

Categories

Resources