Handling numeric property in React form - javascript

I use controlled components in my React forms where I'll tie the value of a field in the form to a property of an object I use to collect data -- see below. For numeric fields in my forms, I like using properties of numeric type but I've discovered an unpleasant behavior and want to see how others handle this.
// In my reducer, I have this object that I use to collect data
newItem: {
description: "",
reward: 0
},
// Then, in my component, I'll tie the input to a property of the object
<input name="reward" type="number" value={this.props.newItem.reward} onClick={e => this.myHandler(e)} />
I'll typically set the initial value of a numeric field to 0 which then renders a 0 in the form for the corresponding field. This is a bit unfriendly because user has to first select the value, then type a new one in -- I realize there are keyboard and mouse tricks one can use but most users don't know them and they will simply use the mouse to highlight the value, then type a new one in.
But the real problem I have is that if the user deletes the 0, we end up with NaN being displayed in the field -- see below before and after.
Other than using a string type for my property then parsing it into a number when I need to, what other options do I have to handle this scenario?

This is just a suggestion, but maybe you could have a onChange() function wich would look if the current value is a number, if not, set the value to 0?
onChange(event){
if( isNaN(event.target.value) ){
// do your stuff here
}
}

You could probably get around it by putting a conditional in the value parameter and assigning a default numeric value:
<input name="reward" type="number" value={isNan(this.props.newItem.reward) ? 0 : this.props.newItem.reward} onClick={e => this.myHandler(e)} />

Instead of using the initial value as you are currently doing, you can display a placeholder value like this:
<input type="text" placeholder="0" ...
More info from Mozilla. The '0' will disappear when a user clicks on it.
To fix the NaN issue, one way would be to store the value as a string instead of as a number, and make the form a controlled component. This is detailed in the React Docs.
For example:
The input:
<input type="text" placeholder="0" value={this.state.value} onChange={this.handleChange} />
handleChange:
handleChange(event) {
if (!isNaN(event.target.value) {
this.setState({value: event.target.value});
}
}
This should ensure that only numbers can be entered in the box.

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?

angular 2 how can keep correct type of form input when stringifying to JSON

I am building dynamic form as in this.
I add another TextboxQuestion which expects a numeric input. On submit, when converting the form values to json as JSON.stringify(this.form.value) (as in dynamic-form.component.ts : onSubmit), the numeric value is being stringified as string.
I mean:
Instead of this (JSON):
{
"mynumber" : 0
}
I got this:
{
"mynumber" : "0"
}
To solve it, onSubmit() I have put control in a loop that checks every form element to convert its value to integer if its type is so.
Another solution was to create a ng model with appropriate types, but as the form is dynamic, I can not create interface/class for every possible form.
I bet that there is much simpler solution to keep correct types when using JSON.stringify.
The issue here is that the form control type of your input is defined as string even if the input is of type number. To update this, make sure to define a new controlType for the number component.
As example, your NumberQuestion component should look similar to:
export class NumberQuestion extends QuestionBase<string> {
controlType = 'number';
type: number;
}
After defining the controlType, make sure to add a new option on the switch statement under the dynamic-form-question.component.html template:
<input *ngSwitchCase="'number'" [formControlName]="question.key"
[id]="question.key" type="number">
Now the final output of your form should be similar to:
{"firstName":"Name","emailAddress":"Email","brave":"good","number":10}
Here is a link to a working example.

Will angular update model value when the viewValue is invalid

I have a form and an input. The input is bound to a value in the controller via ng-model. Now I have a custom validator which invalidates the input's value in certain circumstances. But I can't find out if the model value is updated if my validator invalidates the value. I can't $watch in my controller so I have no idea how I could find out this. Pseudocode here:
(hint: snippet won't run)
class Controller {
constructor() {
this.myVal = 0;
}
}
<input type="number"
id="max"
name="max"
ng-model="ctrl.myVal"
restrict-value="form.min.$viewValue" />
Now my problem can be seen. The restrict-value directive will register a $validator on the ngModel and will return false if the value of the input field is greater than another field (min). If max's value is lower than min's I don't want to update the model value. Can I do this?
In browsers that follow the HTML5 specification, input[number] does not work as expected with ngModelOptions.allowInvalid. If a non-number is entered in the input, the browser will report the value as an empty string, which means the view / model values in ngModel and subsequently the scope value will also be an empty string.
from here

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.

Finding all numeric value from an array of object & perform multiplication or division

I am using ReactJs for building my web application. The application has a state variable which contains the state of the application. My application has a feature where user see three radio buttons (thousands, millions, actual). When user clicks thousands radio button, I need to show all the numeric values in thousands. Same login apply for millions.
The state variable contains array of object. Each object can have its own sets of properties. Each property can be numeric, object, boolean or string.
What I want: I want to go over each property, check if it is numeric type, if true, divide each value by 1000 in case of thousand radio button is checked (similar logic for million button).
What is the most efficient way to achieve desired result.
for formatting, use something like https://www.npmjs.com/package/format-number.
In render function of your component, use array.filter to filter out any non-numeric values, then map the result to something like renderValue function. In this function, check currently selected formatter, which will be saved in state (I'll get to it in a second) by this.state.formatter. Format your value using format-number package and your formatter from this.state.formatter and display it.
Filtering will look something similar to:
this.props.values.filter((value) => !isNaN(value)).map(this.renderValue)
When it comes to setting your formatter to state, create <Input type="radio" name="format" value={1000} onChange={this.handleChange}/>and your handleChange function:
handleChange: function(event) {
this.setState({formatter: event.target.value});
}

Categories

Resources