Stop cursor jumping when formatting number in React - reactjs

I have an input field on my react component that shows the line price for an item (two decimal places with thousands separators). I want the value shown to be in money format when the component first renders and also to be kept in money format as user types in the field.
At the moment I have the following code in my component:
var React = require('react');
import accounting from 'accounting';
MoneyInput = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
value: React.PropTypes.number,
error: React.PropTypes.string,
},
onChange(event) {
// get rid of any money formatting
event.target.value = accounting.unformat(event.target.value);
// pass the value on
this.props.onChange(event);
},
getValue() {
return accounting.formatNumber(this.props.value, 2)
},
render() {
return (
<div className="field">
<input type="text"
name={this.props.name}
className="form-control"
value={this.getValue()}
onChange={this.onChange} />
<div className="input">{this.props.error}</div>
</div>
);
}
});
module.exports = MoneyInput;
That code displays the data correctly formatted, but every time I enter a value the cursor jumps to the end of the number.
I understand why that's happening (I think) and I've read several questions here related to not losing cursor position in JavaScript (here and here for example).
My question is what's the best way to deal with this in React?
I think that ideally I wouldn't want to store the cursor position in state (e.g. I would want these to be Presentation Components in Dan Abramov syntax) so is there another way?

An easy solution for losing cursor/caret position in the React's <input /> field that's being formatted is to manage the position yourself:
onChange(event) {
const caret = event.target.selectionStart
const element = event.target
window.requestAnimationFrame(() => {
element.selectionStart = caret
element.selectionEnd = caret
})
// your code
}
The reason your cursor position resets is because React does not know what kinds of changes you are performing (what if you are changing the text completely to something shorter or longer?) and you are now responsible for controlling the caret position.
Example: On one of my input textfields I auto-replace the three dots (...) with an ellipsis. The former is three-characters-long string, while the latter is just one. Although React would know what the end result would look like, it would not know where to put the cursor anymore as there no one definite logical answer.

onKeyUp(ev) {
const cardNumber = "8318 3712 31"
const selectionStart = ev.target.selectionStart; // save the cursor position before cursor jump
this.setState({ cardNumber, }, () => {
ev.target.setSelectionRange(selectionStart, selectionStart); // set the cursor position on setState callback handler
});
}

I think we can do this at a DOM level.
What I did was provided id to the input field.
There is a property selectionEnd in the input element.
What you can do is just get the input element in the normalize function and get its selectionEnd property
const selectionEnd=inputElm &&inputElem.selectionEnd?inputElm.selectionEnd:0;
And since the problem is only while we press the back button. We add a condition as follows
if(result.length<previousValue.length){
inputElm.setSelectionRange(selectionEnd, selectionEnd)
}
But since this value will be set after we return from the function and the returned value will again be set pushing the cursor to the end, we return just add a settimeout.
setTimeout(() => {
if(result.length<previousValue.length){
inputElm.setSelectionRange(selectionEnd, selectionEnd)
}
}, 50);
I just faced this problem today and seems like a timeout of 50 is sufficient.
And if you want to handle the case of user adding the data in the middle. The following code seems to be working good.
if(result.length<previousValue.length){
inputElm.setSelectionRange(selectionEnd, selectionEnd)
} else if(selectionEnd!==result.length){ // result being the computed value
inputElm.setSelectionRange(selectionEnd, selectionEnd)
}

set value(val) { // Set by external method
if(val != this.state.value) {
this.setState({value: val, randKey: this.getKey()});
}
}
getKey() {
return 'input-key' + parseInt(Math.random() * 100000);
}
onBlur(e) {
this.state.value = e.target.value; // Set by user
if(this.props.blur) this.props.blur(e);
}
render() {
return(
<input className="te-input" type="text" defaultValue={this.state.value} onBlur={this.onBlur} key={this.state.randKey} />
)
}
If you need an Input that's both editable without cursor move and may be set from outside as well you should use default value and render the input with different key when the value is changed externally.

I have the same problem and for me it is because I am using Redux.
This article really explained it well.
https://medium.com/#alutom/in-order-to-understand-what-is-really-happening-it-might-be-helpful-to-artificially-increase-the-e64ce17b70a6
The browser is what manages the input curser and when it detects a new text it automatically kicks the curser to the end, my guess is that the state updates the textfield in a way the triggers that browser behaviour.

Related

React-Phone-Number-Input + React-Hook-Form: How to get current country code in controller validation?

I'm using react-phone-number-input library. The phone number input is not required but if the number is present I wish it could be validated before the form is sent.
If the cellphone field is pristine / left untouched when form is submitted then isValid accepts undefined (valid state).
If country code is changed right away (without actually inputting the number) isValid accepts the selected country's calling code (e.g. +46 for Sweden). This is still a perfectly valid case.
When accessed in the isValid function the phoneCountryCode always holds the previous selected value. So there's always a disparity between the validated phone number and the current country code. I'm not sure if the problem is library-specific, maybe it's my mistake. How do I get rid of the mentioned disparity?
I made a reproduction on CodeSandbox.
import PhoneInput, {
parsePhoneNumber,
getCountryCallingCode
} from "react-phone-number-input";
const [phoneCountryCode, phoneCountryCodeSetter] = useState('DE');
<Controller
name="cellphone"
rules={{
validate: {
isValid: value => {
if(value) {
const callingCode = getCountryCallingCode(phoneCountryCode);
if(! new RegExp(`^\\+${callingCode}$`).test(value)) {
// The parsePhoneNumber utility returns null
// if provided with only the calling code
return !!parsePhoneNumber(value);
}
}
return true;
}
}
}}
control={control}
render={({ field }) => (
<PhoneInput
{...field}
onCountryChange={(v) => phoneCountryCodeSetter(v)}
limitMaxLength={true}
international={true}
defaultCountry="DE"
/>
)}
/>
It's a react specific, nothing wrongs in the library. React never update state immediately, state updates in react are asynchronous; when an update is requested there's no guarantee that the updates will be made immediately. Then updater functions enqueue changes to the component state, but react may delay the changes.
for example:
const [age, setAge] = useSate(21);
const handleClick = () => {
setAge(24)
console.log("Age:", age);
}
You'll see 21 logged in the console.
So this is how react works. In your case, as you change country this "onCountryChange" event triggers updating function to update the state and to validate the phone number but the state is not updated yet that's why it is picking the previous countryCode value(Do console log here).
To understand this more clearly put this code inside your component.
useEffect(() => {
console.log("useEffect: phoneCountryCode", phoneCountryCode);
}, [phoneCountryCode]);
console.log(phoneCountryCode, value); // inside isValid()
useEffect callback will be called when phoneCountryCode value is updated and console.log inside isValid() will be called before the state gets updated.
Hopes this makes sense. Happy Coding!!

Updating a value that's within an array within an array in a state

I'm trying to update the state of the id value in shoeList. Currently I have a textfield that allows for entering of a new ID and I want the state to update when the OK button is clicked.
Here is some of the relevant code:
state = {
editingToggle: false,
shoeList : [
{name: 'bob', id: '123213-0', shoeSize: 'L'}
],
}
<TextInput className='text-area-header' id="textM" width="m" type="text" placeholder={this.state.shoeList[0].id} />
<Button className="ok-button" variant="tertiary" size ='xs' type="button" handleClick={this.saveHeader}>OK</Button>
saveHeader(e) {
this.setState(state=> ({
shoeList[0].name:
}))
alert('Header changed to ' + this.state.shoeList[0].id);
e.preventDefault();
}
I'm not sure what to put in this.setState as I haven't found anything on how to update nested values through a google search. Also whenever I put a value attribute to the TextInput tags it doesn't allow me to edit in the textinput on the webpage anymore. Any help would be great
Consider this example:
saveHeader() {
this.state.shoeList[0].id = 'newid'
this.setState({ shoeList: this.state.shoeList })
}
setState() checks if the values have changed, and if not, it does not update the component. Changing a nested value does not change the reference to the array, which means that simply calling setState() is not enough.
There are two ways around this. The first is to use forceUpdate():
saveHeader() {
this.state.shoeList[0].id = 'newid'
this.forceUpdate()
}
This forces the component to re-render, even though the state didn't change.
The second way is to actually change the shoeList array by creating a new one:
saveHeader() {
let newShoeList = this.state.shoeList.slice()
newShoeList[0].id = 'newid'
this.setState({ shoeList: newShoeList })
}
Using .slice() on an array creates a new array that is completely identical. But because it is a new array, React will notice that the state changed and re-render the component.

Getting doubled value with an onPaste event on an input using React

I have a text input field in my React app and I want to validate its input so I have a validator function working in a handler for the onChange event. I also want to trim whitespace when a value is pasted into the input and then have that run through the onChange validator.
Currently my JSX looks like this:
handleChange(event) {
let { name, value } = event.target
if (typeof this.props.validatorFunction === "function") {
value = this.props.validatorFunction(value) // the validator function in this case is {value => value.replace(" ", "-").toLowerCase()}
}
this.setState({
[name] : value
})
}
handlePaste(event) {
let { name, value, selectionStart, selectionEnd } = event.target
let pastedValue = event.clipboardData.getData("text")
let pre = value.substring(0, selectionStart)
let post = value.substring(selectionEnd, value.length)
value = (pre + pastedValue + post).trim()
this.setState({
[name] : value
})
}
render() {
return (
<input type="text" name="myInput" value={this.state.myInput} onChange={this.handleChange} onPaste={this.handlePaste}></input>
)
}
Regular typing works as expected however, when I paste something such as "test string" I get "test-stringtest string". "test String " with the trailing white-space I get "test-stringtest string ". This works except for the doubling up of the untrimmed but somehow lowercased but not replaced original string. How do I fix this, or what is the proper workflow for this kind of validation using React controlled components?
You need to call event.preventDefault() after handling your paste event. As it is, the paste event is changing the state in handlePaste and then going on to add the pasted text to the input, triggering handleChange.
As requested, I am providing more details on the solution.
It turns out that for my purpose, having an onPaste handler is superfluous. If I removed the onPaste handler, the doubling issue would be resolved, and paste behaviour inherently remains. An example of the code I would write for this today is here: https://jsfiddle.net/tvandinther/sLav0chj/31/
function ValidatedInput(props) {
const format = (value) => typeof props.formatFunction === "function"
? props.formatFunction(value)
: value
return (
<input
value={props.value}
onChange={event => props.setValue(format(event.target.value))}
/>
)
}
Suppose you do need to keep the onPaste handler. In that case, the solution described is to call your shared logic (in this case, formatting the string and setting the state) in both onPaste and onChange handlers but ensure that event.preventDefault() is called in the onPaste handler.

change value of input made with react from chrome extension

I work on Chrome extension, i need to update lot of inputs of an html page made with React from numbers readed from CSV. I cannot update the web site.
-
An example of input copied from the rendered website :
<td><input class="input input_small fpInput" value="29,4"></td>
-
How it's made (not sure 100% about that, had to read the uglified js source)
{
key: "render",
value: function () {
return s.a.createElement("input", {
className: "input input_small fpInput",
value: this.state.value,
onChange: this.handleChange,
onBlur: this.handleSubmit,
onFocus: this.handleFocus
})
}
}
-
Each time you change the input value a function is called and a POST is made to save it.
I want to trigger the onBlur() or onChange() from my extension after i changed the input value to trigger the POST
I tried this :
var el = document. ... .querySelector('input'); // the selector is simplied of course
el.value = 321;
el.onChange(); // ERROR onChange is not a function
el.onchange(); // ERROR onchange is not a function
el.handleChange(); // ERROR handleChange is not a function
Any idea please ?
You can't call a React component's method directly from the DOM element it has rendered. You need to trigger an event that bubbles up so that React can catch it at the document level and process it normally, as it would do with a real one.
✨ Document.execCommand():
As pointed out by #woxxom in the comments, the easiest way to do that might be to focus the inputs and then use Document.execCommand():
const input1 = document.getElementById('input1');
const input2 = document.getElementById('input2');
input1.focus();
document.execCommand('insertText', false, 'input1');
input2.focus();
document.execCommand('insertText', false, 'input2');
<input id="input1" />
<input id="input2" />
⚒️ Manually Dispatching Events:
Otherwise, you might try manually dispatching a change, input and/or blur event using the Event() constructor in those fields after you change their value.
Also, note Event()'s second optional argument contains a field, bubbles, that is false by default. You need that one to be true. Otherwise, this won't work, as React is listening for events on the document.
Additionally, you might need to use Element.setAttribute() too, as that will update the value attribute on the DOM (the initial value on the field), while element.value will update the current value (and so the display value). Usually, though, it's not needed. For more on this see What is the difference between properties and attributes in HTML?.
This approach might have some timing issues you might need to handle using setTimeout when updating multiple inputs, so if the first one works, I'd go for that one instead.
const input1 = document.getElementById('input1');
// This updates the value displayed on the input, but not the DOM (you can do that with setAttribute,
// but it's not needed here):
input1.value = 'input1';
// Dispatch an "input" event. In some cases, "change" would also work:
input1.dispatchEvent(new Event('input', { bubbles: true }));
// It looks like only one field will be updated if both events are dispatched
// straight away, so you could use a setTimeout here:
setTimeout(() => {
const input2 = document.getElementById('input2');
input2.value = 'input2';
input2.dispatchEvent(new Event('input', { bubbles: true }));
});
<input id="input1" />
<input id="input2" />
To elaborate a bit more on #varoons answer, which is factually correct albeit a bit short on explanation.
You can do so by injecting (dispatching, in browser terms) the event into the dom:
// Needs setAttribute to work well with React and everything, just `.value` doesn't cut it
// Also changed it to a string, as all attributes are strings (even for <input type="number" />)
el.setAttribute("value", "321");
// As #wOxxOm pointed out, we need to pass `{ bubbles: true }` to the options,
// as React listens on the document element and not the individual input elements
el.dispatchEvent(new Event("change", { bubbles: true }));
el.dispatchEvent(new Event("blur", { bubbles: true }));
This will actually call all the listeners, even those made with React (as is the case in your de-uglyfied code ;)) or made with simple element.onChange = () => {...} listeners.
Example: https://codesandbox.io/s/kml7m2nn4r
el.dispatchEvent(new CustomEvent("change"))

Dynamically set a property value for an Input in Semantic UI React

I have an Input element that I want to display an error on when the form validation fails.
<Input ref="amount" error={false} />
When the user enters an incorrect amount, I want to change "error" to "true". How can this be done?
I have tried:
this.refs.amount.props.error = true;
Which seems bad but I'm not sure how else. If I add a conditional statement in the definition of the Input element, that seems to only evaluate once and then remain the same. Do I need to force an update on the element? If so, how?
Yes it's possible to validate the input when the form is submitted.
All you need is to keep track on input value and use same approach as #SajithDilshan for the input error.
this.state = {
error: false,
value: ''
}
...
render(){
return
...
<Input
ref="amount"
value={this.state.value}
error={this.state.error}
/>
...
}
Then onSubmit should looks like:
onSubmit(e){
const isError = this.state.value === '';
this.setState({error: isError});
// rest of your logic
}
Hope it will help!
Use the onChange() method on the input as below.
<Input ref="amount" onChange={this.onInputChange} error={this.state.error} />
After that implement the onInputChange() method as below in your component.
onInputChange = (e) => {
if (e.target.value === "") { // logic to validate the input
this.setState({error: true});
} else {
this.setState({error: false});
}
}
Note that this will add error property to the state.
Further, you should not modify the props within a component. Props are passes from parent component to the child component as immutable inputs.
This is not exactly the answer, but still:
This type of fiddling with each possible state of form element (valid, invalid, warning, show tooltip, was edited, in focus, left focus, was submitted, submit failed or not, etc) becomes to much trouble when the form grows beyond 1 input field.
I would suggest to use redux-form package that integrates with semantic-ui-react` almost perfectly and provided you have provided it with the validate function does everything else for you. It takes some time to understand the basics of it, but it really pays.

Resources