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/caret 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?
Answers
Handling the cursor position issue in a React component, especially with formatted input fields like money inputs, can be a bit tricky due to how React handles the rendering of input fields and the natural behavior of the browser.
Approach to Preserve Cursor Position
To maintain the cursor position while formatting the input dynamically, you can utilize a technique that involves managing the selection range of the input field. Here’s a refined approach to achieve this:
-
Using
getSelection
andsetSelectionRange
:- These are standard DOM methods that allow you to get and set the selection (cursor position) within an input element.
-
Refactoring
onChange
Handler:- Update the
onChange
handler to preserve the cursor position when the input value changes due to formatting.
- Update the
Here’s how you can refactor your MoneyInput
component to preserve the cursor position:
import React, { Component } from 'react';
import accounting from 'accounting';
class MoneyInput extends Component {
static propTypes = {
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
value: React.PropTypes.number,
error: React.PropTypes.string,
};
onChange = (event) => {
// Get the current cursor position
const cursorPosition = event.target.selectionStart;
// Remove any money formatting
const unformattedValue = accounting.unformat(event.target.value);
// Pass the value on
this.props.onChange({
...event,
target: {
...event.target,
value: unformattedValue,
},
});
// Restore cursor position
setTimeout(() => {
const newPosition = cursorPosition - (event.target.value.length - unformattedValue.length);
event.target.setSelectionRange(newPosition, newPosition);
}, 0);
};
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>
);
}
}
export default MoneyInput;
Explanation:
-
onChange
Handler:- Cursor Position: Before updating the input value, capture the current cursor position using
event.target.selectionStart
. - Formatting: Remove any formatting using
accounting.unformat
. - Updating Value: Update
event.target.value
with the unformatted value and pass the modified event tothis.props.onChange
. - Restoring Cursor Position: After the input value updates (due to formatting), use
setTimeout
to asynchronously set the cursor position back to its original location usingsetSelectionRange
.
- Cursor Position: Before updating the input value, capture the current cursor position using
-
getValue
Method:- Formats
this.props.value
into a money format with two decimal places usingaccounting.formatNumber
.
- Formats
Key Points:
-
Asynchronous Update: Using
setTimeout
ensures that the cursor position is set after the input value has been updated, avoiding conflicts with React's internal rendering cycle. -
Preservation of Cursor Position: By calculating the new cursor position based on the difference in length between the formatted and unformatted values, you ensure that the cursor stays where the user expects it.
This approach allows you to maintain a clean separation between presentation (formatting) and behavior (cursor management), adhering to the principles of smart/dumb components in React.