React Input onChange before onKeyDown - reactjs

const {useMemo, useState, createRef} = React;
const InputPIN = (props) => {
const inputRefs = useMemo(() => Array(4).fill(0).map(i=> createRef()), []);
const [values, setValues] = useState(Array(4).fill(''));
function handleChange(e, index){
const typed = e.target.value.toString();
setValues((prev)=>
prev.map((i,jndex) => {
if(index === jndex) return typed[typed.length-1];
else return i;
})
);
if (typed !=='' && inputRefs[index + 1]) inputRefs[index + 1].current.focus();
}
function handleBackspace(e, index) {
if(e.keyCode === 8){
if(inputRefs[index-1]){
inputRefs[index - 1].current.focus();
}
}
}
return (
<label className="InputPIN">
{
new Array(4).fill(0).map((i,index)=>(
<input style={{width:50}} key={index} value={values[index]} onKeyDown={(e)=>handleBackspace(e,index)} type="text" ref={inputRefs[index]} onChange={(e)=>handleChange(e,index)} /> ) )
}
</label>
)
}
const App = () => {
return (
<InputPIN />
)
}
ReactDOM.render(<App />, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
I am making an InputPIN component.
When I typed a text, input focusing would be moved to the next input.
and When I typed the backspace, input focusing would be moved to the previous input after removing a present text. But It doesn't work.
on my code, I am using onKeyDown() and onChange().
and I guess onKeyDown() has a higher priority than onChange().
so, I've tried to change onKeyDown() to onKeyUp().
but it has a side effect that I don't want.

EDIT: The whole answer has been updated after the discussion in comments.
I think you can just use a custom handler function, and rewrite a little the two handlers methods:
const manualUpdateInputs = (index) => {
const newValues = [...values];
newValues[index] = '';
inputRefs[index].current.focus();
}
const handleChange = (e, index) => {
const newValues = [...values];
const typed = e.target.value.toString();
newValues[index] = typed;
if (typed !=='' && inputRefs[index + 1]) {
inputRefs[index + 1].current.focus();
}
setValues(newValues);
}
const handleBackspace = (e, index) => {
if(e.keyCode === 8){
if(inputRefs[index-1] && e.target.value === '') {
manualUpdateInputs(index - 1);
}
}
}
I've changed the way you update values but you can keep your way, it doesn't change. What matters is the use of manualUpdateInputs.
The "trick" was that handleBackspace is triggered anytime you click a key in the keyboard, but handleChange was not triggered if you clicked a backspace with an empty <input>.
Tell me if the behavior obtained with this implementation is what you wanted (it makes sense to me, but maybe it's not what you wanted).

Related

Modifying user input value in React controlled component

I've been stuck on this error for a long time, so I would appreciate some help. Here is a minimally reproducible exmaple:
import "./App.css";
import React, { useState } from "react";
import $ from "jquery";
function App() {
const [value, setValue] = useState("");
const focusHandler = () => {
$("input").on("keypress", (e) => {
let copy = value;
if (e.key === ".") {
e.preventDefault();
copy += " ";
setValue(copy);
}
});
};
const blurHandler = (event) => {
$("input").off("keypress");
setValue(event.target.value);
};
const changeHandler = (event) => {
setValue(event.target.value);
};
return (
<div>
<input
value={value}
onFocus={focusHandler}
onBlur={blurHandler}
onChange={changeHandler}
/>
</div>
);
}
export default App;
On input focus, I'm adding an event listener to look for a . keypress and append a tab (4 spaces) to the input if it is pressed. But when I press . multiple times, the input gets stuck at the first tab, and doesn't move any further (e.g. input permanenetly shows 4 spaces). Using console.log shows me that the value state doesn't seem to be updating in focusHandler and reverts to the original value ("").
An important note is that switching to a class-based component with this.state makes it work. Any insight as to why this is happening?
As mentioned in the comments, jQuery is the wrong tool for the job. Bringing in jQuery is the same as calling DOM methods directly: it's circumventing React and adding additional listeners on top of the ones React already gives you. You can expect misbehavior if you're setting state from multiple handlers unrelated to React. Once you're in React, use the tools it gives you (refs, effects, handlers) to solve the problem.
Worst case scenario is when an approach appears to work, then fails in production, on other people's machines/browsers, when refactoring from classes to hooks or vice-versa, in different versions of React, or for 1 out of 1000 users. Staying well within the API React gives you better guarantees that your app will behave correctly.
Controlled component
For manipulating the input value, you can use both onKeyDown and onChange listeners. onKeyDown fires first. Calling event.preventDefault() inside of onKeyDown will block the change event and ensure only one call to setState for the controlled input value occurs per keystroke.
The problem with this the input cursor moves to the end after the component updates (see relevant GitHub issue). One way to deal with this is to manually adjust the cursor position when you've made an invasive change to the string by adding state to keep track of the cursor and using a ref and useEffect to set selectionStart and selectionEnd properties on the input element.
This causes a brief blinking effect due to asynchrony after the render, so this isn't a great solution. If you're always appending to the end of the value, you assume the user won't move the cursor as other answers do, or you want the cursor to finish at the end, then this is a non-issue, but this assumption doesn't hold in the general case.
One solution is to use useLayoutEffect which runs synchronously before the repaint, eliminating the blink.
With useEffect:
const {useEffect, useRef, useState} = React;
const App = () => {
const [value, setValue] = useState("");
const [cursor, setCursor] = useState(-1);
const inputRef = useRef();
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {selectionStart: start} = event.target;
const {selectionEnd: end} = event.target;
const v = value.slice(0, start) + pad + value.slice(end);
setValue(v);
setCursor(start + pad.length);
}
};
const onChange = event => {
setValue(event.target.value);
setCursor(-1);
};
useEffect(() => {
if (cursor >= 0) {
inputRef.current.selectionStart = cursor;
inputRef.current.selectionEnd = cursor;
}
}, [cursor]);
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
ref={inputRef}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
With useLayoutEffect:
const {useLayoutEffect, useRef, useState} = React;
const App = () => {
const [value, setValue] = useState("");
const [cursor, setCursor] = useState(-1);
const inputRef = useRef();
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {selectionStart: start} = event.target;
const {selectionEnd: end} = event.target;
const v = value.slice(0, start) + pad + value.slice(end);
setValue(v);
setCursor(start + pad.length);
}
};
const onChange = event => {
setValue(event.target.value);
setCursor(-1);
};
useLayoutEffect(() => {
if (cursor >= 0) {
inputRef.current.selectionStart = cursor;
inputRef.current.selectionEnd = cursor;
}
}, [cursor]);
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
ref={inputRef}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Uncontrolled component
Here's another attempt using an uncontrolled component. This doesn't have the blinking problem because the DOM element's .value property is synchronously set at the same time as the .selectionStart property and is rendered in the same repaint.
const App = () => {
const pad = ". ";
const onKeyDown = event => {
if (event.code === "Period") {
event.preventDefault();
const {target} = event;
const {
value, selectionStart: start, selectionEnd: end,
} = target;
target.value = value.slice(0, start) +
pad + value.slice(end);
target.selectionStart = start + pad.length;
target.selectionEnd = start + pad.length;
}
};
return (
<div>
<p>press `.` to add 4 spaces:</p>
<input
onKeyDown={onKeyDown}
/>
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
input {
width: 100%;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Don't mix direct DOM manipulation, whether that's vanilla JavaScript or jQuery, with React. There is no need to add an event handler with jQuery here, because your methods are already event handlers. Just use them directly:
const focusHandler = (e) => {
// handle the event here!
}
My solution:
const changeHandler = (event) => {
const key = event.nativeEvent.data;
if (key === ".") {
event.preventDefault();
const initialValue = event.target.value.split(".")[0];
console.log(initialValue);
setValue(initialValue + " ");
} else {
setValue(event.target.value);
}
};

How to prepending before icon for text area for React component

I'm trying to implement React version of TextArea which appends "$" every-time I press Return/Enter.
I'm having hard time prepending a sign # or % or dollar every time someone presses enter. How can I go about this?
This is my basic attempt but I'm kind of lost:
const MyComponent = () => {
const [name, setName] = React.useState('');
return (
<div>
<textarea value={name} onChange={(e) => { setName(e.target.value) }} />
</div>
);
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
Ok so I had a bit of time on my hands and thought this could be a good learning experience. So I present to you: MoneyInputList
import './css/MoneyInputList.css'
import { useState, useEffect } from 'react';
let latestAdded = 'moneyInputList-input-0';
let lastSize = 0;
const MoneyInputList = () => {
const [recordList, setRecordList] = useState({data: ['']});
const handleFormSubmit = (e) => {
e.preventDefault();
if(recordList.data[recordList.data.length-1] !== ''){
setRecordList({data: [...recordList.data, '']})
}
};
useEffect(() => {
if(lastSize !== recordList.data.length)
document.getElementById(latestAdded).focus();
lastSize = recordList.data.length;
}, [recordList]);
return (
<form autoComplete='off' onSubmit={handleFormSubmit}>
<div className="main-container">
{recordList.data.length > 0 &&
recordList.data.map((record, iter) => {
latestAdded = "moneyInputList-input-"+iter;
return (
<div key={"moneyInputList-field-"+iter} className="record-field">
<div className="record-sign">$</div>
<input className="record-input" id={"moneyInputList-input-"+iter} value={recordList.data[iter]} onChange={(e) => {
if(e.target.value === '' && iter !== recordList.data.length-1){
let modifiedData = [];
recordList.data.forEach((e,i) => {
if(i !== iter)
modifiedData.push(e);
});
setRecordList({data: modifiedData});
return;
}
const filteredValue = e.target.value.split('').filter(e=>(e.charCodeAt() >= '0'.charCodeAt() && e.charCodeAt() <= '9'.charCodeAt()));
let formattedValue = [];
filteredValue.forEach((elem, i) => {
if((filteredValue.length - i) % 3 === 0 && i !== 0)
formattedValue.push(',');
formattedValue.push(elem);
});
formattedValue = formattedValue.join('');
e.target.value = formattedValue;
let myData = recordList.data;
myData[iter] = e.target.value;
setRecordList({data: myData});
}} type="text"/>
</div>
)})
}
</div>
<input style={{flex: 0, visibility: 'collapse', height: 0, width: 0, padding: 0, margin: 0}} type="submit"/>
</form>
)
}
export default MoneyInputList;
This component should do what you need it to do. It is not the best code but it works. You can see it working here. Of course you might still need to change some stuff in order for it to fit in your codebase and maybe implement redux, but the general idea is here. You use it by typing whatever number you want pressing enter will create a new line and deleting the content of a line will remove it.
I hope I understood correctly what you are trying to do, here is a super scuffed version of it. You might need to change the code a bit to fit your use case.
import { useState, useEffect } from "react";
export default function App() {
const [textValue, setTextValue] = useState("");
const [displayedTextValue, setDisplayedTextValue] = useState("");
useEffect(() => {
let splitTextValue = textValue.split("\n");
splitTextValue = splitTextValue.map((line, iter) => {
if (iter === splitTextValue.length - 1) return line;
if (line[0] !== "$") return "$ " + line;
return line;
});
setDisplayedTextValue(splitTextValue.join("\n"));
}, [textValue]);
return (
<div>
<textarea
value={displayedTextValue}
onChange={(e) => {
setTextValue(e.target.value);
}}
/>
</div>
);
}
Here is a version working with key event that I think is cleaner when handling thing depending on keys.
Here is the repro on Stackblitz and here is the code :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const enterKeyCode = 'Enter';
const backspaceKeyCode = 'Backspace';
const [val, setVal] = React.useState('$ ');
const [keyCodeEvent, setKeyCodeEvent] = React.useState();
React.useEffect(() => {
if (keyCodeEvent) {
// Handle numpad 'enter' key
if (keyCodeEvent[0].includes(enterKeyCode)) {
setVal(`${val}\n$ `);
} else if (keyCodeEvent[0] === backspaceKeyCode) {
setVal(`${val.slice(0, -1)}`);
} else {
setVal(`${val}${keyCodeEvent[1]}`);
}
}
}, [keyCodeEvent]);
return (
<div>
{/* Empty onChange to prevent warning in console */}
<textarea onKeyDown={e => setKeyCodeEvent([e.code, e.key])} value={val} onChange={() => {}} />
</div>
);
};
render(<App />, document.getElementById('root'));
I read your comments on Marko Borković 's answer so I handled the backspace but he is totally right when saying you should build a special component for this. It will be way easier to improve and cleaner. You are not safe from some others bugs if you want to add features to your component.

How do I detect when I've reached the end of an array in a react functional component?

I have a simple component that contains an array of animal names (cow, horse, chicken) and a button that when clicked, increments the index of the array to display the name of the next animal. I want to know once I've reached the end of the array so that I can redirect the user. How can I achieve this?
import React, { useState } from 'react'
export default function Test() {
const [array, setArray] = useState(['cow', 'horse', 'chicken'])
const [index, setIndex] = useState(0)
const handleClick = () => {
setIndex(prevIndex => prevIndex + 1)
}
return (
<div>
<button onClick={handleClick}>Next animal</button>
<h2>{array[index]}</h2>
</div>
)
}
I had tried to add a conditional statement to the callback function along the following lines, but it did not work:
const handleClick = () => {
if(index < array.length){
setIndex(prevIndex => prevIndex + 1)
} else {
alert("We've reached the end of the array, redirect user!")
}
}
Any help would be greatly appreciated!
You're off by 1. The last index will be array.length - 1, so compare against that:
const App = () => {
const [array, setArray] = React.useState(['cow', 'horse', 'chicken'])
const [index, setIndex] = React.useState(0)
const handleClick = () => {
if(index === array.length - 1){
alert("We've reached the end of the array, redirect user!")
} else {
setIndex(index + 1);
}
}
return (
<div>
<button onClick={handleClick}>Next animal</button>
<h2>{array[index]}</h2>
</div>
)
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
Whenever you will hit the handleClick function the index state will be updated by one. According to the condition applied inside the handleClick function whenever the index state number reaches equal reaches above the numbers of elements present inside an array it will show an alert.
const handleClick = () => {
setindex(index + 1)
if (index >= array.length - 1 ) {
alert("We've reached the end of the array, redirect user!")
}
}

Custom hook not using useState() properly?

I'm trying to make one of those custom useKeyPress hooks, and I want my keydown handler to fire on every individual button press, i.e. holding down a key won't trigger it a million times. I'm trying to do this by having a flag that indicates whether to call the handler, callHandler - it should only be set to true when the target key is released/the component just mounted. Below is a simplified version of what I'm trying to do:
const useKeyPress = (target)=>{
const [callHandler, setCallHandler] = useState(true);
const keyDown = (e) =>{
if(e.key === target && callHandler){
setCallHandler(false); //This line never seems to work properly???
console.log(callHandler, setCallHandler);
//call the keyDown handler
}
}
const keyUp = (e)=>{
if(e.key === target)
setCallHandler(true);
}
useEffect(()=>{
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup' keyUp);
return ()=>{
document.removeEventListener('keydown', keyDown);
document.removeEventListener('keyup', keyUp);
}
},[]);
}
useKeyPress('a');
My issue is that callHandler is never set to false. That console log immediately after setCallHandler just spams the debug screen with (true, ...) when I hold down the 'a' key, meaning that despite the code block running callHandler is somehow always false. Any ideas as to why this isn't working?
Im not sure if i get it right , anyway i made this code hope helps u a bit.
EDIT
Change it to custom hook example, maybe useful for someone in future.
const useKeyEvent = (target) =>{
const [keyTarget,setKeyTarget] = React.useState(target);
const [playedKey,setPlayedKey] = React.useState(null);
const [keyDone,setKeyDone] = React.useState(false);
const keyUp = (e) => {
setPlayedKey(null);
setKeyDone(false)
};
const keyDown = (e) => {
const key = e.key;
if (key !== playedKey) {
if(key === keyTarget){
setKeyDone(true);
}
setPlayedKey(key);
}
};
React.useEffect(() => {
document.addEventListener("keydown", keyDown);
document.addEventListener("keyup", keyUp);
return () => {
document.removeEventListener("keydown", keyDown);
document.removeEventListener("keyup", keyUp);
};
}, [keyUp, keyDown,keyDone]);
return [playedKey,keyTarget,setKeyTarget,keyDone];
}
const App = () => {
const [playedKey,keyTarget,setKeyTarget,keyDone] = useKeyEvent("a");
return (
<div>
<h3 style={{color:keyDone?"green":"black"}}>Press {keyTarget} key ! Key : {playedKey}</h3>
<input type="text" onChange={(e)=>setKeyTarget(e.currentTarget.value)} maxLength="1" value={keyTarget}/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

React useRef with Array for dynamic

I am trying to make the component that the focus moves to the next input when each letter inputted.
I think I need multiple ref like an array but I don't know about it.
It's a sample code for the question.
function PIN({length, onChange, value}){
const inputEl = React.useRef(null);
function handleChange(e){
onChange(e);
inputEl.current.focus();
}
return (
<div>
{
new Array(length).fill(0).map((i)=>(
<input type="text" ref={inputEl} onChange={handleChange} />
))
}
</div>
)
}
You can create multiple refs
function PIN({length, onChange, value}){
const inputRefs = useMemo(() => Array(length).fill(0).map(i=> React.createRef()), []);
const handleChange = index => (e) => {
//onChange(e); // don't know about the logic of this onChange if you have multiple inputs
if (inputRefs[index + 1]) inputRefs[index + 1].current.focus();
}
return (
<div>
{
new Array(length).fill(0).map((inp, index)=>(
<input type="text" ref={inputRefs[index]} onChange={handleChange(index)} />
))
}
</div>
)
}
The ref on input is equivalent to a callback function. You can pass a method to him. The parameter received by this method is the input dom element, which you can store in an array.
import React from "react";
import "./styles.css";
export default function App() {
const inputEl = React.useRef([]);
function handleChange(i){
inputEl.current[i+1].focus();
}
return (
<div>
{
new Array(3).fill(0).map((n,i)=>(
<input
key={i}
type="text"
ref={ref=>inputEl.current.push(ref)}
onChange={()=>handleChange(i)}
/>
))
}
</div>
)
}
In your inputs, you can pass a function to the ref parameter, this will allow you to store all of your refs in an array:
let myRefs = [];
const saveThisRef = (element) => {
myRefs.push(element);
}
Then you can pass your function to each input you render:
<input type="text" ref={saveThisRef} onChange={handleChange} />
Then you can advance to the next input in the onChange handler:
// Find the index of the next element
const index = myRefs.indexOf(element) + 1;
// Focus it
if (index < myRefs.length) myRefs[index].focus();
Re-rendering the component that holds the dynamic Refs list with a different number of refs raises an exception ("Rendered more hooks than during the previous render"), as you can see in this example:
https://codesandbox.io/s/intelligent-shannon-u3yo6?file=/src/App.js
You can create a new component that renders a single and holds it's own single ref, and use the parent element to manage the current focused input, and pass this data to you'r new component, for example.
Here is an example that would actually work:
const { useState, useCallback, useEffect, useRef } = React;
const Pin = ({ length, onChange, value }) => {
const [val, setVal] = useState(value.split(''));
const [index, setIndex] = useState(0);
const arr = [...new Array(length)].map(
(_, index) => index
);
const myRefs = useRef(arr);
const saveThisRef = (index) => (element) => {
myRefs.current[index] = element;
};
function handleChange(e) {
const newVal = [...val];
newVal[index] = e.target.value;
if (index < length - 1) {
setIndex(index + 1);
}
setVal(newVal);
onChange(newVal.join(''));
}
const onFocus = (index) => () => {
const newVal = [...val];
newVal[index] = '';
setIndex(index);
setVal(newVal);
onChange(newVal.join(''));
};
useEffect(() => {
if (index < myRefs.current.length) {
myRefs.current[index].focus();
}
}, [index, length, myRefs]);
return arr.map((index) => (
<input
type="text"
ref={saveThisRef(index)}
onChange={handleChange}
onFocus={onFocus(index)}
value={val[index] || ''}
maxLength="1"
key={index}
/>
));
};
const App = () => {
const [value, setValue] = useState('');
const onChange = useCallback(
(value) => setValue(value),
[]
);
console.log('value:', value);
return (
<Pin
length={5}
value={value}
onChange={onChange}
/>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
All answers will shift focus to next input when you correct an already set value. The requirement is that focus should shift when a letter is inputted, not when you remove a value.

Resources