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.
Related
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!!
One thing that confuses me is the onChange event.
As the code below, onChange event work with handleChange function.
So what confuses me here is the event parameter.
class ControlledInput extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "empty"
};
// change code below this line
this.handleChange = this.handleChange.bind(this);
// change code above this line
}
// change code below this line
handleChange(event) {
this.setState({
input: event.target.value
})
}
// change code above this line
render() {
return (
<div>
{ /* change code below this line */}
<input type="text" value={this.state.input} onChange={this.handleChange}></input>
{ /* change code above this line */}
<h4>Controlled Input:</h4>
<p>{this.state.input}</p>
</div>
);
}
};
Does every function work with onChange event need to have a parameter. If the answer is yes, why?
I replaced the keyword ‘event’ with some other keywords and it still works normally. So is it okay if I use a different word instead of the keyword ‘event’?
Does every function work with onChange event need to have a parameter. If the answer is yes, why?
They don't need to use the event parameter, therefore you don't need to declare that parameter in the callback function and the function will get called anyways (the invoker will still pass the parameter, of course, but you can ignore it if you don't need it). However, if you want to know what has changed, then you probably will want to use the info of the event parameter, right?
I replaced the keyword ‘event’ with some other keywords and it still works normally. So is it okay if I use a different word instead of the keyword ‘event’?
The word event is not a reserved word in JavaScript, you can name the parameter of the function however you want, it will still work just the same, yes.
Maybe the following example will help you understand how callbacks work with JS.
Consider the following function... What it does is it lets you register a function that will get invoked every certain number of milliseconds. Whenever the function gets invoked you will receive the current epoch time:
const timerSubscription = (ms, fn) => {
const id = setInterval(() => {
fn(Date.now());
}, ms);
return () => clearInterval(id);
}
So you could use it like this:
let count = 0;
const unsubscribe = timerSubscription(1000, () => {
count++;
console.log('One second passed');
if (count === 5) {
unsubscribe();
}
});
Or you could use it like this:
let start = Date.now();
const unsubscribe = timerSubscription(1000, (now) => {
console.log('One second passed');
if (now + 5000 >= start) {
unsubscribe();
}
});
They do almost exactly the same thing, one uses the argument that the invoker passes to it and the other one doesn't. Either way the invoker is always passing the argument.
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.
I have a mapped list of input fields:
<FormControl
name={row_index}
value={barcode.barcode}
placeholder="Barcode - then Enter"
onChange={this.onChange}
onKeyPress={this._handleKeyPress}
disabled={barcode.submitted}
/>
I am currently using onKeyPress to handle submit:
_handleKeyPress = (e) => {
if (e.key === 'Enter') {
const name = e.target.name;
const barcodes = this.state.barcodes;
const this_barcode = barcodes[name];
let apiFormatted = {"barcode": this_barcode.barcode, "uid": this.props.currentSession}
this.postBarcodeAPI(apiFormatted, name)
}
}
I am attempting to focus on the next input field after the current one is successfully submitted. React documentation has an example for manually setting focus on a single input field using ref={(input) => { this.textInput = input; }} />. I have tried using this[‘textInput’+‘1’].focus() (using computed property names, but am getting an error that function is invalid.
EDIT
Per Chase's answer, I am linking to the autofocus documentation, although it doesn't work in this case.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/autofocus
My working solution:
const focusing = index === lastSubmittedIndex + 1 ? true : false;
const refText = focusing || index === 0 ? input => input && input.focus() : null;
<FormControl
name={row_index}
value={barcode.barcode}
placeholder="Barcode - then Enter"
onChange={this.onChange}
onKeyPress={this._handleKeyPress}
disabled={barcode.submitted || barcode.apiCalling}
inputRef={refText}
/>
What I corrected in my code:
1) I am supposed to use inputRef instead of ref for Bootstrap's FormControl component, see here.
2) I am using ilya-semenov's very neat code.
Update
I have other buttons on the page, when user presses them and is at bottom of page, page jumps up to top. Not sure why.
Unless you've set a ref with the key textInput1 then this won't work. Another suggestion would be to move all of the inputs into a separate component and then you can use this.props.children to traverse all your inputs and grab the input at whatever position you want.
EDIT:
You can also use the autoFocus prop to determine if an input should be focused or not.
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.