Caret Jumping to End When Rejecting Input - javascript

I have an input for a value. I want to validate the new value entered by the user and prevent the value from appearing in the input if it's invalid. For instance, the value should be numeric, and hence can only contain numbers, dots, and commas.
I have a validation function which returns true/false depending on whether a value meets the criteria (works as intended):
function validateInput(input) {
let allow = true
const splitValuesByDecimal = input.split(',')
if(allow) allow = /^[\d|,|.]*?$/.test(input) // allow only digits, dots, and commas
if(allow) allow = splitValuesByDecimal.length <= 2 // allow only 1 decimal separator (comma)
if(allow && splitValuesByDecimal.length > 1) allow = splitValuesByDecimal.at(-1).length <= 2 // allow only maximum of 2 decimal characters
return allow
}
In my input field, I call a custom handleChange function which ensures that the new input is valid before storing it in the state variable.
Rendering input
<input value={inputVal} onChange={handleChange} />
Handling input change
function handleChange(e) {
const value = e.target.value
if(validateInput(value)) {
setInputVal(value)
}
}
If an invalid input is inserted and validation fails, the value in my input field does not change (as intended), but the caret jumps to the end of the input field (not desirable).
Attempted Solution
I have tried to fix the caret jumping behaviour by adopting this answer where I created a cursor state variable which keeps track of the intended caret position:
function MyComponent() {
const [inputVal, setInputVal] = useState('')
const [cursor, setCursor] = useState(null)
const inputRef = useRef()
useEffect(() => {
if (inputRef.current) inputRef.current.setSelectionRange(cursor, cursor);
}, [inputRef, cursor, inputVal])
function validateInput(input) {
...
}
function handleChange(e) {
const value = e.target.value
if(validateInput(value)) {
setCursor(e.target.selectionStart)
setInputVal(value)
}
else {
setCursor(e.target.selectionStart - 1)
}
}
return (
<input
ref={inputRef}
value={inputVal}
onChange={handleChange} />
)
}
With this solution, the caret persists at the correct position the first time an invalid value is entered, but as I keep on subsequently pressing the same key inserting invalid value over and over again, after the first time, the caret moves back to the end of the input. This is because the useEffect is not called the second and subsequent times.
How can I make this work so that caret always stays at the same position regardless of how many times the user attempts to insert an invalid value?

Update :-
Sorry my bad, I haven't looked at the question completely. However what I was saying is also correct but not in current case. Please find the updated code below. Below code should also be more performant as it does not involve re-renders on every change
import React, { useState, useRef } from 'react';
export function App(props) {
const inputRef = useRef()
const oldVal = useRef(null)
function validateInput(input) {
let allow = true
const splitValuesByDecimal = input.split(',')
if(allow) allow = /^[\d|,|.]*?$/.test(input) // allow only digits, dots, and commas
if(allow) allow = splitValuesByDecimal.length <= 2 // allow only 1 decimal separator (comma)
if(allow && splitValuesByDecimal.length > 1) allow = splitValuesByDecimal.at(-1).length <= 2 // allow only maximum of 2 decimal characters
console.log(allow)
return allow
}
function handleChange(e) {
const valueNew = e.target.value
const pos = e.target.selectionStart - 1
if(validateInput(valueNew)) {
oldVal.current = valueNew
} else {
inputRef.current.value = oldVal.current
inputRef.current.setSelectionRange(pos, pos)
}
}
return (
<div key='mydiv' className='App'>
<input key='MyFixedKeyValue' ref={inputRef} onChange={handleChange} />
</div>
);
}
I also changed the same on playcode code link
React re-renders the whole component on setstate call. In your case values are being kept as it is a controlled input. But behind the scene react is basically creating a new input field every time you are entering a value. To force react to use the same field pass key prop with constant value.
<input
key='MyFixedKeyValue'
ref={inputRef}
value={inputVal}
onChange={handleChange} />

Related

How do I properly handle 'NaN' value in input type='number' in React?

Just creating a simple currency converter (React + Typescript).
Everything works good. But I'm bit misunderstanding 1 thing: how to clear input (where you enter amount of money) dynamically properly?
Code:
const App = () => {
...
const [amount, setAmount] = useState(1)
const [amountInFromCurrency, setAmountInFromCurrency] = useState(true)
let fromAmount, toAmount
if (amountInFromCurrency) {
fromAmount = amount
toAmount = parseFloat((amount * exchangeRate).toFixed(2))
} else {
toAmount = amount
fromAmount = parseFloat((amount / exchangeRate).toFixed(2))
}
return (
<Card
amount={fromAmount}
/>
<Card
amount={toAmount}
/>
)
}
const Card = ({ amount }) => {
return (
<input type="number" value={amount || 0} onChange={changeInput}/>
)
}
When I start clean the input to the end it becomes 0 in both inputs as expected. But when I start entering the numbers in the input after cleaning it (when it shows 0) they start appearing after 0 but not instead. Example: shows "0", I start entering the numbers, shows "0123" instead "123". The 0 disapears only if I highlight all the numbers with cursor and enter another number or it disappears if I push inner/outer-spin-buttons in input.
As you can see my approach here for input value is value={amount || 0}. If I put only value={amount} the mistake that 'You have NaN in value' appears when I clean input to the end. I don't need that mistake.
What I need: cleaning inputs fully without mistake or if I show 0 after cleaning I need to disapper it correctly when I start entering other numbers like "123" but not "0123"
UPDATE:
Code for input change:
const handleFromAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(parseInt(e.target.value))
setAmountInFromCurrency(true)
}
const handleToAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(parseInt(e.target.value))
setAmountInFromCurrency(false)
}

How can I create a numeric field where the focus out adds 3 decimal digits?

I have a text field:
<Grid item xs={12} sm={6}>
<BzTextField
fullWidth
label="dec"
value={company.dex}
/>
</Grid>
what I am trying to do is to create a new component so I can create a new input type number which must add 3 decimal digits (ex. 3.3 becomes 3.300). I am having problem with how to implement something like this. Should I use Hooks or is there an easier way.
also the validation should be onChange and not on keyUp.
You can do it by creating a controlled input and using input's onBlur feature, which will execute onBlur function when you are done editing the input. This function will change add 3 decimal digits to the number.
function App() {
const [input, setInput] = React.useState("")
const onBlur = () => {
setInput(parseFloat(input).toFixed(3))
}
return (
<div >
<body>
<input
type="number"
value={input}
onChange={e=>setInput(e.target.value)}
onBlur={onBlur}
/>
</body>
</div>
);
}
Try This:
const x = 3.3;
const y = x.toFixed(3);
console.log(y);
// result 3.300
Explanation:
here toFixed(n) method used to extend value.
Where n is the number you want to extend value of number.
Click here for more info
Here's one way you could do it using material. I made the assumption that you don't want the text formatted until focus leaves the control. The onChange event in material fires on every key press so this makes it difficult to format using that event alone.
import React, { useState } from 'react';
import { TextField } from '#material-ui/core';
const BzTextField = ({ defaultValue = '' }) => {
const [value, setValue] = useState(defaultValue);
const handleBlur = (e) => {
let parsedValue = parseFloat(e.target.value);
if (parsedValue) {
setValue(parsedValue.toFixed(3));
}
}
return (
<TextField
type="number"
fullWidth
label="dec"
value={value}
onBlur={handleBlur}/>
);
};
export default BzTextField;

React TS - Max character limit for textarea

So I have 2 issues. I've created a textarea component that will also keep track of how many characters the user has typed into the textarea.
Issue 1:
I'm trying to see if the current input length the user has typed is more than or equal to(>=) my maxLength prop if (value.length >= maxLength), but I get this error:
var maxLength: number | undefined
Object is possibly 'undefined'.ts(2532)
Could somebody explain why I'm getting this?
Issue 2:
Currently when you type a single character into the textarea, the character limit will remain at 0, and will only start to increment after the second character has been typed. Also if you type some text in the textatea and then delete it, it says the character limit is 1/X when it should be 0/X. How can I fix this?
Code:
import * as React from "react";
import "./styles.css";
interface ITextarea {
maxLength?: number;
}
const Textarea: React.FC<ITextarea> = ({ maxLength }) => {
const [value, setValue] = React.useState("");
const [charLimit, setCharLimit] = React.useState(`0 / ${maxLength}`);
const handleCharLimit = () => {
if (value.length >= maxLength) {
setCharLimit("You have reached the maximum number of characters!");
} else {
setCharLimit(`${value.length} / ${maxLength}`);
}
};
return (
<>
<textarea
value={value}
onChange={event => {
setValue(event.target.value);
handleCharLimit();
}}
maxLength={maxLength}
/>
{maxLength && <span>{charLimit}</span>}
</>
);
};
export default function App() {
return (
<div className="App">
<Textarea maxLength={50} />
</div>
);
}
Here's a CodeSandBox, forks are appretiated :)
Object is possibly 'undefined'.ts(2532)
ITextarea defines maxLength as an optional parameter. If you don't pass a value for it, its value will be undefined.
This happens because React state updates aren't immediate, they happen on the next render pass. You're calling handleCharLimit, but when you invoke it, value hasn't actually been updated yet, even though it's called after setValue. You should be using useEffect or useMemo to update your charLimit in response to a change in value.
See an updated version here: https://codesandbox.io/s/hungry-fermi-bsmny

Change Array Elements Only Three Times, Repeat

What is the best way to change each element of an array based on the length of the array?
For example:
User #1 input = "XYZVC"
Expected Output = "BLABL"
User #2 input = "XYZVCAD"
Expected Output = "BLABLAB"
I want B to replace index[0], L to replace index[1], and A to replace index[2]. The part I'm struggling with the most is also getting it to repeat if the user input is longer than 3.
My Attempts
I've attempted to split() the input into an array, shift() it and push() it into a new array without any luck.
I've pasted in my code below for even more detail:
import React from 'react'
class UserInput extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
handleChange = (event) => {
this.setState({value: event.target.value})
}
handleSubmit = (event) => {
alert('Your input has been submitted: ' + this.state.value)
event.preventDefault()
}
badRobot = () => {
//Check length of user input
//Add B, L, A, matching the length of the user input
//Store it within a new variable
//Plug that into the bad robots output
let checkedInput = this.state.value
let checkedArray = checkedInput.split('')
const newArr = checkedInput.shift()
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<label>
<p>Say Something</p>
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
<div>
<h3>Good Robot</h3>
<p>I hear you saying {this.state.value}. Is that correct?</p>
<h3>Bad Robot</h3>
<p>.</p>
<h3>Kanyebot 5000</h3>
<p>I'm gonna let you finish but Beyonce is {this.state.value}.</p>
</div>
</div>
)
}
}
export default UserInput
UPDATE
As it turns out, there is a string function, padStart (works with padEnd too) that will do exactly that pretty easily :
const input1 = "XYZVCAD"
const input2 = "erkjguherkluagheruhg"
function mask(str){
return ''.padStart(str.length, 'BLA');
}
console.log(mask(input1))
console.log(mask(input2))
One way to achieve it is to use the repeat function and then cut out your string depending on the original string length :
const input1 = "XYZVCAD"
const input2 = "erkjguherkluagheruhg"
function mask(str){
return 'BLA'.repeat(str.length / 3 + 1).substring(0, str.length)
}
console.log(mask(input1))
console.log(mask(input2))
The repeat function will repeat your string x times, as you will see in the docs linked above. Here, since your string is 3 characters long, we only need to repeat your string depending on the length of the original one, divided by 3. I am adding one since a division like 5/3 will be rounded to 1, leading to BLA even though your string is longer.
The last step, substring, will simply cut your string to the exact same length as the original one.
Here is another way of achieving it, by splitting your string into an array, and giving it the correct letter by using the modulus operator :
const input1 = "XYZVCAD"
const input2 = "erkjguherkluagheruhg"
function mask(str){
return str.split('').map((ch, index) => 'BLA'[index % 3]).join('')
}
console.log(mask(input1))
console.log(mask(input2))
This isn't code, but it's the approach I would take:
Split the input into a char array
For loop through it, with index starting at 0
If index + 1 MOD 3 = 0, set value to 'A' (array[i+1]%3 === 0)
If index + 1 MOD 2 = 0, set value to 'L'
Otherwise set value to 'B'
Return a string of the joined array items (array.join)
var myString = "somestringofsomelength";
var replacementsArr=["B","L","A"];
function replacer(str){
repString="";
for(var i=0; i<str.length;i++){
repString+=replacementsArr[i % replacementsArr.length];
}
return repString;
}
console.log(replacer(myString));
That's the way I would do it. It's using remainder division to partition the string from being 12345678 in index relative to replacementsArr to 12312312
You could change it straight away
const letters = ['B','L','A'];
handleChange = (event) => {
const length = event.target.value.length;
const letter = letters[length%3];
this.setState({value: this.state.value+letter});
}

How to get newly typed text in react-native text input?

When user types text in TextInput, onChangeText and onChange are called.
I am getting event from onChange callback and can get newly updated text from event.nativeEvent.text.
I am getting text from onChangeText callback and can get newly updated text from it.
But I only want newly entered text/letters. How to get this?
For example:-
lets assume we have som as text in TextInput, now as soon as user type e, I want only this e instead of whole text some.
Any work around?
We can get newly typed letters via
Use onSelectionChange props in your Input. We can get an event from onSelectionChange from which we can get cursor position. Through cursor position and full text we can easily get newly entered letters.
Example:-
<Input
...
onChangeText={this.handleMessageChange}
onSelectionChange={this.handleMessageSelectionChange}
...
/>
And methods
handleMessageChange = (message: string) => {
this.setState({ message });
};
handleMessageSelectionChange = (event: Object) => {
const { selection } = event.nativeEvent;
this.setState({ selection });
};
Now whenever you want newly entered letters, get it
getNewlyEnteredLetters = (): string => {
const { selection, message } = state;
const { start, end } = selection;
return start === end ? message[start -1 ] : message[message.length - 1]
}; // start === end implies that new letters are entered and not selection is made.
One way to solve this in pure js try this with the string
getNewChar(str){
return str[str.length - 1];
}
To get only the newly entered text you can use the onChange function of TextInput as below:-
<TextInput
onChange={(event) => {console.log(event.nativeEvent.data)}}
onChangeText={(val) => {console.warn(val)}}
/>
As specified the onChangeText will give you the complete text but you can use the event from onChange to get the newly eneterd letter.
EDIT
According to your react native newer versions you get the complete new text in event.nativeEvent.text and no data in event.nativeEvent.data. So for your requirement you can use the code below for now :-
<TextInput
onChange={(evnt)=>{
let newText = [];
let oldText = [];
for(let i=0;i<evnt.nativeEvent.text.length;i++) {
newText.push(evnt.nativeEvent.text[i]);
if(this.state.val[i]) {
oldText.push(this.state.val[i]);
}
}
let newLetter = '';
for(let j=0;j<newText.length;j++) {
if(newText[j] !== oldText[j]) {
newLetter = newText[j];
break;
}
}
console.log(newLetter);
}}
onChangeText={(val)=>{
console.log(val);
this.setState({val});
}}
/>
You will get the newly entered letter inside your console of newLetter. I will let you know if i find anything else related to this but for now you can get working with this :)

Categories

Resources