React OnChange for every field - is there a better way - javascript

I call an API, get a payload and load it into state. Eg:
Payload : {
id: 1,
firstName: 'Craig',
surname: 'Smith',
anotherField: 'test data',
....
}
(Note, there are around 20 fields in real life)
On my react screen, I have the fields to display this data. At the moment, for every field, I have an onChange function. So:
this.firstnameOnChange = this.firstnameOnChange.bind(this);
and then the function:
firstnameOnChange() { ....}
Is there a simpler pattern to maybe lessen the amount of methods I need to create? Some sort of generic OnChange event that takes a name, and the value, which can then be used to update the state?
I can bind to something like:
myOnChange(fieldName, value) {
// Somehow find the 'fieldName' in the state and update it's value with 'value'
}
Is this a valid pattern? Or is there a better way? Or should I stick to separate OnChange methods for each field?
If I can go generic, how would I find the name of the field in the state to update?

No this is not recommended to write onChange for every field. You can write a generic onChange method like this:
handleChange= name => event => {
this.setState({payload: {...this.state.payload, [name] : event.target.value}});
}
for example you have an input like this :
<Input onChange={this.handleChange('firstName')} placeholder="Please Enter" />
PS here we are invoking handleChange directly so we use currying means a new instance being created for every invocation. There is another approach by extract name form event.target and create a name attribute in your input. Then you don't need to pass input name explicitly.

Since the state is an object that means you can update it's key and value.
All you have to do is pass down the correct parameters to a generic update function which will update the state.
Here's one way you can do it:
https://codesandbox.io/s/v382v6l483
Since your payload is an entry one level down, we'll be using the spread operator and spread out the payload object.
Update the value that you need and finally returns the payload to setState function to update the state and rerender the component.
You can bind this function to each field and keep your state updated.
Read more here on handling events and binding functions: https://reactjs.org/docs/handling-events.html

Related

React Final Form Pass Additional Parameters to Form State

React Final Form uses a render prop pattern in its form component, and I am trying to understand how I can modify it to pass it additional arguments.
According to it's documentation, it passes the following props to the render function - including (crucially) the FormState.
Let's take a look at my particular implementation, focusing on the render function:
<Form
onSubmit={() => {console.log("wow coolio you submitted a form!")}}
initialValues={initData}
validate={validateMyForm}
render={({ handleSubmit, reset, submitting, pristine, values }) => {
formSubmitHandler = async () => {
await handleSubmit()
return values
}
return(
//... form components with the values prop passed into them below
//...i.e. <Part_1_Of_Form values={values}/>
If I understand correctly, you will note I destruct the render prop object in the JSX {({})} and get the values from the FormState.
I then pass them to various modular form components below - like <Part_1_Of_Form/> in order to make each part of the form react to state changes in other parts of the form. But my problem is I want it to react to more than the values. For example, if I want to display a label conditionally based on if another label option (or another option) is selected in another part of the form, I can't access it because I only get access to the values - not the labels.
Example, with a currency selection tool to propagate throughout the form using state:
<Field
name="part_one.currency"
component={MyRFFInputSelectComponent}
options={[{value: 'USD', label: '$ (USD)', symbol: '$'}, {value: 'EUR', label: '€ (EUR)', symbol: '€'}]}
required={true}
className="form-control">
</Field>
I can pass the value (let's say USD) around other parts of the form, using the render props and the values part of that returned object - but I don't want to. I want to pass the symbol - the value (USD) belongs to the database (it's only USD because the DB wants it as a string like that) - but the other parts of the form should display the symbol instead of the value once I select my currency in the top part of the form.
Example of what I have to do now elsewhere in the form:
<Field
...
append={values['part_one']['currency']}
Example of what I want to do:
<Field
...
append={symbols['part_one']['currency']}
Or maybe even better:
<Field
...
append={allFormData(notjustvalues)['part_one']['currency']['symbol']}
So that each input that needs a price can show a currency in the appended label.
Does this usecase make sense?
Is there another way to pick up the symbol or a way to add it to the FormState? I think its probably bad for perf to pass something like allFormData around just to get one custom symbol. I can't use the symbol for the value because my backend developer will cry. 😢
If I'm understanding you correctly, your value for part_one.currency needs to be the entire {value: 'USD', label: '$ (USD)', symbol: '$'} object.
Then you can do values.part_one.currency.symbol elsewhere in your form. If you only want the value to be 'USD' when you submit, you'll need to handle that in your onSubmit function. Does that help?

React container not binding correctly

I'm currently working on a search bar and the value of the input is not displaying in the search bar. When I log the value in the console it only displays one character at a time
I'm assuming that the way that I have set up my state and term is incorrect and rewriting the state everytime it fires, rather than updating it?
Cheers
You have an extra = sign in your setState function.
this.setState is a function, not a variable. You have to pass argument into it, not assign a new value.
Change it into:
this.setState({ term: event.target.value })

Vue.js get all v-model bindings

With Vue.js you can you bind form input to a property:
https://v2.vuejs.org/v2/guide/forms.html
I am working on a project where I 'dynamically' generate a form though. Here the v-model bindings are just attributes that are being set based on the name that an input has.
The problem though is that I am left wondering how to properly get these v-model bindings. So far all I have see is people manually setting a property on their Vue instance for the binding:
new Vue({
el: '...',
data: {
checkedNames: []
}
})
Is it possible to somehow grab all of the v-model bindings though without setting up the data properties manually?
When creating a dynamic form with a variation an dynamic number of fields this would help out a lot.
I guess one option is simply retrieving the name atributes from the input with javascript but I am hoping there might be something in place already since v-model is probably calling a setter that might already be known in the Vue instance.
Reworked the example from the answer:
https://jsfiddle.net/yMv7y/2477/
According to the official documentation:
it’s possible to add reactive properties to a nested object using the Vue.set(object, key, value) method.
This means that your starting data can be something like:
{
customForm: [],
values: {}
}
Let's assume customForm is an array of field objects that describe a bunch of fields in your custom form. It's empty at the beginning because you say your form will be created dynamically.
Once you generate that dynamic data, all you need to do is assign it to customForm and loop through it to add reactive properties with the aforementioned Vue.set method.
Here's a JSFiddle with an example. I'm using this.$set, but that's just an alias for Vue.set.

Why does min attribute cause ngChange to be called?

I have the following input field:
<input type="number"
class="menu-control validate"
style="width: 50px;"
ng-disabled="!ctrl.editable()"
min="1"
ng-change="ctrl.updateBookingPriceRequest()"
ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 2000, 'blur': 0 }}"
ng-model="extra.quantity" />
My problem is the min directive. While it's there, angular starts repeatedly calling ng-change without the input having changed at all (not good since I'm performing an ajax call on change). If I remove min it works just fine, and I don't have the same problem with max.
It doesn't seem to matter if the model is above or below min initially.
Can anyone see something that I can't?
Edit:
I tried making my change function do nothing, and it stopped the problem, so it must be due to my code. But what I don't understand is why it works fine without min!
this.updateBookingPriceRequest = function () {
_this.prices.getBookingPrice(_this.bookingPrice).then(function (response) {
if (response.successful) {
_this.bookingPrice = response.data;
_this.bookingPrice.mooringExtras.bookingExtras.forEach(function (extra) {
var feature = _this.features.filter(function (f) { return f.featureId === extra.mooringFeatureId; })[0];
extra.unitOfMeasureId = feature.unitOfMeasureId;
extra.pricedQty = feature.pricedQuantity;
extra.pricingType = feature.pricingType;
});
if (_this.bookingPrice.mooringDiscounts) {
_this.bookingPrice.mooringDiscounts.forEach(function (discount) {
discount.discountName = _this.harborDiscounts.filter(function (x) { return x.id === discount.discountModelId; })[0].name;
});
}
}
else
_this.Error.showErrorMessage('Error getting booking price: ' + response.message);
});
};
The "extra" object on which the model is a property is changed in the function, however the "quantity" property remains the same. Could this cause ng-change to be triggered?
Edit to show how objects are defined (see comment by valepu):
The extra object(s) are in an array (my input field is inside a repeater, but in my current test there is only one object in the array), which is defined on a property called mooringExtras, which in turn is a property of a bookingPrice object, which is updated via an http request when ng-change gets called (see code). I know it gets complicated, my apologies for not knowing how to simplify it better.
The extra object contains a number of properties, with "quantity", a number, being the model for the input.
Here is an JSON of the extra object:
{"id":401,"bookableFeatureId":13,"mooringFeatureId":4,"featureName":"Wi-fi","bookingId":1104,"booked":true,"price":100.00,"totalAmount":300.00,"days":8,"quantity":3,"currencyUnit":"SEK","created":1460542055177}
Every time ng-change is called the bookingPrice object is changed, however, the value of extra.quantity remains the same.
I have just realized that in your onChange function you do this:
_this.bookingPrice = response.data;
Which, according to what you wrote in your question, is the object containing the array you iterate on to create your inputs.
When you completely replace the object, ng-repeat will create the inputs from scratch. When you have min set in your input this will trigger ng-change on input creation if the starting input is not valid (angular will set the ng-model to undefined in this case), which will change the whole array, which will trigger ng-repeat again, recreating inputs with a min attribute, which will trigger ng-change again and so on...
Normally ng-repeat generates an hash of the object to track changes on the data it's iterating on, if you completely replace it then it will think you deleted the old object and put in a new one (even though they have the same data), by using track by extra.id will tell ng-repeat that even though you replaced the object, they actually didn't change (they still have the same .id) and won't recreate the objects from scratch but, this is a fix but it's probably a good practice to just replace the values of the current array.
I have managed to recreate your issue in this plunkr: http://plnkr.co/edit/XyEyGTvuYKyz1GGmWjuP?p=preview
if you remove the line:
ctrl.objects = [{quantity: 0, price: 0, booked: true}, {quantity: 0, price: 0, booked: true}];
it will work again
I'm still not quite sure why the problem only occurred with the min attribute on the field, but by adding "track by extra.id" to the ng-repeat that wrapped the input field, I solved the problem. I guess when the "extra" object, on which the model was a property, changed, angular regenerated the input field, triggering ng-change. By tracking by an unchanging id, angular doesn't need to regenerate the input field since the id remains the same, thus not triggering ng-change.
I'll accept this as my answer, but if anyone can explain why it worked without min, I will happily accept their answer instead.

Struggling for Backbone syntax in collection events

I have a collection, I can do this successfully ('this' is the collection instance):
this.on('change:username', function(model,property,something){
// model is backbone model that changed
// property is the property that changed (username in this case)
// something is some empty mystery object that I can't identify
}
however what I want to do is this:
this.on('change', function(model,property,something){
// model is backbone model that changed
// ***how can I read the properties that changed here?***
// something is some empty mystery object that I can't identify
}
the problem is that in the second case, I can't get the property that changed...maybe that's because it's potentially multiple property changes all at once.
How can I capture that properties that changed in the second case? is this possible?
The only way I know how to do this would be
this.on('change', function(model, something){
// something object is unidentifiable
var changed = model.changed; //hash of changed attributes
}
so my other question is: what is that mystery object "something"? It is just an empty object...?
You have a couple of options you can use in general change events:
Backbone.Model#hasChanged: This will allow you to see if a model attribute has changed in the context of this change event. If so, you can get its new value and apply it to the view (or whatever other context) as needed.
Backbone.Model#changedAttributes: This will allow you to get all changed attributes since the last set() call. When called with no parameters, it is a defensively cloned copy of the changed hash; you can also pass in a hash of parameters and get only what is different about the model relative to that set of key/value pairs.
Backbone.Model#previous: This will allow you to get the previous value of a model attribute during a change event.
Backbone.Model#previousAttributes: This will allow you to get all the previous values of a model during a change event. You could use this to completely undo a change (by calling set with the result of this function) if you wanted to.
By the way, the third parameter of the change:attr event (and the second of change) is an options object, which can be useful if you want to specify custom values that can be read by your event handlers. There are also a number of standard options Backbone will handle specially. See the documentation for Backbone.Model#set for more information on the specific options, and take a look at the Backbone event list to see the callback signatures expected when those events are triggered.

Categories

Resources