React how to clear input and fire onChange event - reactjs

I'm creating react component library with own basic components and some logic on the top of that. I have wrapped classic input field with clearable button. I need implement _onClearInput, but idk how to fire onChange event with empty string value. Is there anyone who is able to help me?
import React, { ComponentPropsWithoutRef, useState } from 'react';
export interface MyInputProps extends ComponentPropsWithoutRef<'input'> {
}
const MyInput = React.forwardRef<HTMLInputElement, MyInputProps>(({
value,
onChange,
...rest
}, ref) => {
const [_value, _setValue] = useState(value || '');
const _onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
_setValue(event.target.value);
if(onChange) {
onChange(event);
}
}
const _onClearInput = () => {
_setValue('');
// I need trigger onChange event somehow with event.target.value = ''
}
return(
<div className={`input-${_value === '' ? 'is-not' : 'is'}-set`}>
<input ref={ref} value={_value} onChange={_onChange} {...rest}/>
<button onClick={_onClearInput}>ClearInput</button>
</div>
);
});
export default MyInput;

Related

react-hook-form: Mapping onChange to a different property

I have a radixUI component that takes in an onPressedChange prop. How can I map this to onChange in react-hook-form?
onChange from react-hook-form doesn't accept a custom function that satisfies the typing when I try as per below.
import * as Toggle from "#radix-ui/react-toggle";
import { useForm } from "react-hook-form";
const MyComponent = () => {
const { register, watch, getValues } = useForm();
return (
<div>
<Toggle.Root {...register("basic")} />
<code>watchAll: {JSON.stringify(watch(), null, 2)}</code>
</div>
);
};
I tried spreading it out like below but my functions don't satisfy the onChange typings...
import * as Toggle from "#radix-ui/react-toggle";
import { useForm } from "react-hook-form";
const MyComponent = () => {
const { register, watch, getValues } = useForm();
const { onChange, onBlur, name, ref } = register('basic');
return (
<div>
<Toggle.Root onPressedChange={onChange} onBlur={onBlur} name={name} ref={ref} />
// ^This doesn't work.
// Neither does `onPressedChange={pressed=>onChange(pressed)}`
<code>watchAll: {JSON.stringify(watch(), null, 2)}</code>
</div>
);
};
The onChange prop from react-hook-form accepts the following ChangeHandler:
type ChangeHandler = (event: {
target: any;
type?: any;
}) => Promise<void | boolean>;
and the typing for onPressedChange is:
type OnPressedChangeHandler = (pressed: boolean) => void
One way to map it is to simply use the setValue method from useForm() instead of onChange.
You could use something like this since you know the name of the element, "basic".
const MyComponent = () => {
const { register, watch, setValue } = useForm();
const { onBlur, name, ref } = register("basic");
const onPressedChange = (boolean) => {
setValue("basic", boolean);
};
return (
<div>
<Toggle.Root
onPressedChange={onPressedChange}
onBlur={onBlur}
name={name}
ref={ref}
/>
<code>watchAll: {JSON.stringify(watch(), null, 2)}</code>
</div>
);
};
Working Sandbox:
https://codesandbox.io/p/sandbox/quizzical-spence-ll8mqm

Can't access forwareded Ref value in parent component React typescript

I have a situation where i have created a component for input. This is a custom component and i want to access the value entered by user in this input in the parent component(where i am using it).
I am forwarding ref from this Input Component but the parent component is receiving the complete input not the value. How can i use the value.
Below is my code.
Input.tsx
interface AuxProps {
id :''
}
const Input = React.forwardRef<HTMLInputElement,AuxProps>((props, ref) => {
return (
<input
id={props.id}
ref = {ref}
defaultValue = '1'
type='number'
></input>
);
});
export default Input;
HeaderComponent.tsx
const HeaderComponent= () => {
const inputAmount = useRef<HTMLInputElement>(null);
const addProductHandle = (event: any) => {
event.preventDefault();
console.log(inputAmount.current.value); //<----Error:- object is possibly null
};
return (
<form className={classes["form"]}>
<Input id="1s" ref={inputAmount}></Input>
<button onClick={addProductHandle}> + ADD </button>
</form>
);
};
export default HeaderComponent;
Not sure how can i use this ref value.
You were close.
Let's take a look on useRef return type:
interface RefObject<T> {
readonly current: T | null;
}
According to this type signature, current property might be T (in our case HTMLInputElement) or null.
This is why you are using typescript - to avoid errors on PROD.
Since current might be null, TS asks you to double check if current exists.
you can add ? or if condition:
import React, { useRef, MouseEventHandler } from 'react'
interface AuxProps {
id: string
}
const Input = React.forwardRef<HTMLInputElement, AuxProps>((props, ref) => {
return (
<input
id={props.id}
ref={ref}
defaultValue='1'
type='number'
></input>
);
});
const HeaderComponent = () => {
const inputAmount = useRef<HTMLInputElement>(null);
const addProductHandle: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
console.log(inputAmount.current?.value); // ok
if (inputAmount.current) {
console.log(inputAmount.current.value); //ok
}
};
return (
<form >
<Input id="1s" ref={inputAmount}></Input>
<button onClick={addProductHandle}> + ADD </button>
</form>
);
};
export default HeaderComponent;
Btw, you can use MouseEventHandler<HTMLButtonElement> for click handlers. See my example

Render data in React Hooks

Hello I'm try to Render data in a component to another component, which are siblings to one another. with useState Hook(rff) based on component code (rcf)
index.js -> is entry point, that calls only one component App, as we have no route
App.js -> is the parent component, which has two child, Certification and Panel
Certification.js -> takes input
Panel -> renders data from certification
I know i have problem with handleFromCert (this is my handle change function)
here my code -rff
https://codesandbox.io/s/zen-paper-gil-xcyj3?file=/src/
here the code that based on rcf and work fine
https://codesandbox.io/s/elegant-shtern-362ki?file=/src/
I corrected the code and now it works!
handleFromCert in App.js should receive name and value;
value2 in the Panel component in App.js is passed with an error;
handleFromCert in Certifications.js setValue changes incorrectly.
Certifications.js
import React, { useState } from "react";
const Certifications = (props) => {
const [value, setValue] = useState({
value1: "",
value2: ""
});
const handleFromCert = ({ target: { value, name } }) => {
setValue(prevState => ({ ...prevState, [name]: value }));
props.handleFromCert(name, value);
};
return (
<div>
<input name="value1" onChange={handleFromCert} />
<input name="value2" onChange={handleFromCert} />
<div>
Inside certificate
<div>{value.value1}</div>
<div>{value.value2}</div>
Certificate ends
</div>
</div>
);
};
export default Certifications;
App.js
import React, { useState } from "react";
import Certifications from "./Certifications";
import Panel from "./Panel";
const App = () => {
const [value, setValue] = useState({
value1: "",
value2: ""
});
const handleFromCert = (name, value) =>
setValue((prevState) => ({ ...prevState, [name]: value }));
return (
<div>
{value.value1}
{value.value2}
<Certifications handleFromCert={handleFromCert} />
<Panel value1={value.value1} value2={value.value2} />
</div>
);
};
export default App;
The problem is that you're not passing the event as the argument, you're passing the value and your function is expecting the event. In your Certification component change this:
const handleFromCert = (e) => {
setValue({
[e.target.name]: e.target.value
});
props.handleFromCert((e.target.name, e.target.value));
};
To this:
const handleFromCert = (e) => {
setValue({
[e.target.name]: e.target.value
});
props.handleFromCert(e);
};
Your function handleFromCert is expecting the event, and you're just passing the value which is a string and cannot be destructured like the event.
Here's the sandbox working for me:
https://codesandbox.io/s/zen-paper-gil-forked-r01fh

How to pass data from child to parent component using react hooks

I have a Parent component and couple of child components. I need to disable or enable the button in the parent based on the ErrorComponent. If there is an error then I disable the button or else I enable it. I believe we can pass callbacks from the child to parent and let the parent know and update the button property. I need to know how to do the same using react hooks? I tried few examples but in vain. There is no event on error component. If there is an error (props.errorMessage) then I need to pass some data to parent so that I can disable the button. Any help is highly appreciated
export const Parent: React.FC<Props> = (props) => {
....
const createContent = (): JSX.Element => {
return (
{<ErrorPanel message={props.errorMessage}/>}
<AnotherComponent/>
);
}
return (
<Button onClick={onSubmit} disabled={}>My Button</Button>
{createContent()}
);
};
export const ErrorPanel: React.FC<Props> = (props) => {
if (props.message) {
return (
<div>{props.message}</div>
);
}
return null;
};
I'd use useEffect hook in this case, to set the disabled state depending on the message props. You can see the whole working app here: codesandbox
ErrorPanel component will look like this:
import React, { useEffect } from "react";
interface IPropTypes {
setDisabled(disabled:boolean): void;
message?: string;
}
const ErrorPanel = ({ setDisabled, message }: IPropTypes) => {
useEffect(() => {
if (message) {
setDisabled(true);
} else {
setDisabled(false);
}
}, [message, setDisabled]);
if (message) {
return <div>Error: {message}</div>;
}
return null;
};
export default ErrorPanel;
So depending on the message prop, whenever it 'exists', I set the disabled prop to true by manipulating the setDisabled function passed by the prop.
And to make this work, Parent component looks like this:
import React, { MouseEvent, useState } from "react";
import ErrorPanel from "./ErrorPanel";
interface IPropTypes {
errorMessage?: string;
}
const Parent = ({ errorMessage }: IPropTypes) => {
const [disabled, setDisabled] = useState(false);
const createContent = () => {
return <ErrorPanel setDisabled={setDisabled} message={errorMessage} />;
};
const handleSubmit = (e: MouseEvent) => {
e.preventDefault();
alert("Submit");
};
return (
<>
<button onClick={handleSubmit} disabled={disabled}>
My Button
</button>
<br />
<br />
{createContent()}
</>
);
};
export default Parent;

React controlled input cursor jumps

I am using React and have formatted a controlled input field, which works fine when I write some numbers and click outside the input field. However, when I want to edit the input, the cursor jumps to the front of the value in the input field. This only occur in IE, and not in e.g. Chrome. I've seen that for some programmers the cursor jumps to the back of the value. So I think the reason that my cursor is jumping to the front is because the value is aligned to the right instead of to the left in the input field. Here is a senario:
My first input is 1000
Then I want to edit it to 10003, but the result is
31000
Is there a way to controll that the cursor should not jump?
Here's a drop-in replacement for the <input/> tag. It's a simple functional component that uses hooks to preserve and restore the cursor position:
import React, { useEffect, useRef, useState } from 'react';
const ControlledInput = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState(null);
const ref = useRef(null);
useEffect(() => {
const input = ref.current;
if (input) input.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;
Taking a guess by your question, your code most likely looks similar to this:
<input
autoFocus="autofocus"
type="text"
value={this.state.value}
onChange={(e) => this.setState({value: e.target.value})}
/>
This may vary in behaviour if your event is handled with onBlur but essentially its the same issue. The behaviour here, which many have stated as a React "bug", is actually expected behaviour.
Your input control's value is not an initial value of the control when its loaded, but rather an underlying value bound to this.state. And when the state changes the control is re-rendered by React.
Essentially this means that the control is recreated by React and populated by the state's value. The problem is that it has no way of knowing what the cursor position was before it was recreated.
One way of solving this which I found to work is remembering the cursor position before it was re-rendered as follows:
<input
autoFocus="autofocus"
type="text"
value={ this.state.value }
onChange={(e) => {
this.cursor = e.target.selectionStart;
this.setState({value: e.target.value});
}
}
onFocus={(e) => {
e.target.selectionStart = this.cursor;
}
}
/>
This is my solution:
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
//get reference for input
this.nameRef = React.createRef();
//Setup cursor position for input
this.cursor;
}
componentDidUpdate() {
this._setCursorPositions();
}
_setCursorPositions = () => {
//reset the cursor position for input
this.nameRef.current.selectionStart = this.cursor;
this.nameRef.current.selectionEnd = this.cursor;
};
handleInputChange = (key, val) => {
this.setState({
[key]: val
});
};
render() {
return (
<div className="content">
<div className="form-group col-md-3">
<label htmlFor="name">Name</label>
<input
ref={this.nameRef}
type="text"
autoComplete="off"
className="form-control"
id="name"
value={this.state.name}
onChange={event => {
this.cursor = event.target.selectionStart;
this.handleInputChange("name", event.currentTarget.value);
}}
/>
</div>
</div>
);
}
}
export default App;
This is an easy solution. Worked for me.
<Input
ref={input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)}
value={this.state.inputtext}
onChange={(e)=>{
this.cursor = e.target.selectionStart;
this.setState({inputtext: e.target.value})
/>
Explanation:
What we are doing here is we save the cursor position in onChange(), now when the tag re-renders due to a change in the state value, the ref code is executed, and inside the ref code we restore out cursor position.
If you're using textarea, then here's the hook based on Daniel Loiterton's code using TypeScript:
interface IControlledTextArea {
value: string
onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
[x: string]: any
}
const ControlledTextArea = ({ value, onChange, ...rest }: IControlledTextArea) => {
const [cursor, setCursor] = useState(0)
const ref = useRef(null)
useEffect(() => {
const input: any = ref.current
if (input) {
input.setSelectionRange(cursor, cursor)
}
}, [ref, cursor, value])
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setCursor(e.target.selectionStart)
onChange && onChange(e)
}
return <textarea ref={ref} value={value} onChange={handleChange} {...rest} />
}
As (I think) others have mentioned, React will keep track of this if you make your changes synchronously. Unfortunately, that's not always feasible. The other solutions suggest tracking the cursor position independently, but this will not work for input types like 'email' which will not allow you to use cursor properties/methods like selectionStart, setSelectionRange or whatever.
Instead, I did something like this:
const Input = (props) => {
const { onChange: _onChange, value } = props;
const [localValue, setLocalValue] = useState(value);
const onChange = useCallback(
e => {
setLocalValue(e.target.value);
_onChange(e.target.value);
},
[_onChange]
);
useEffect(() => {
setLocalValue(value);
}, [value]);
// Use JSX here if you prefer
return react.createElement('input', {
...props,
value: localValue,
onChange
});
};
This allows you to delegate the cursor positioning back to React, but make your async changes.
My cursor jumped always to the end of the line. This solution seems to fix the problem (from github):
import * as React from "react";
import * as ReactDOM from "react-dom";
class App extends React.Component<{}, { text: string }> {
private textarea: React.RefObject<HTMLTextAreaElement>;
constructor(props) {
super(props);
this.state = { text: "" };
this.textarea = React.createRef();
}
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const cursor = e.target.selectionStart;
this.setState({ text: e.target.value }, () => {
if (this.textarea.current != null)
this.textarea.current.selectionEnd = cursor;
});
}
render() {
return (
<textarea
ref={this.textarea}
value={this.state.text}
onChange={this.handleChange.bind(this)}
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Here is my solution
const Input = () => {
const [val, setVal] = useState('');
const inputEl = useRef(null);
const handleInputChange = e => {
const { value, selectionEnd } = e.target;
const rightCharsCount = value.length - selectionEnd;
const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
const newPosition = formattedValue.length - rightCharsCount;
setVal(formattedValue);
setTimeout(() => {
inputEl.current.setSelectionRange(newPosition, newPosition);
}, 0);
};
return <input ref={inputEl} value={val} onChange={handleInputChange} />;
};
// Here is a custom hook to overcome this problem:
import { useRef, useCallback, useLayoutEffect } from 'react'
/**
* This hook overcomes this issue {#link https://github.com/reduxjs/react-redux/issues/525}
* This is not an ideal solution. We need to figure out why the places where this hook is being used
* the controlled InputText fields are losing their cursor position when being remounted to the DOM
* #param {Function} callback - the onChangeCallback for the inputRef
* #returns {Function} - the newCallback that fixes the cursor position from being reset
*/
const useControlledInputOnChangeCursorFix = callback => {
const inputCursor = useRef(0)
const inputRef = useRef(null)
const newCallback = useCallback(
e => {
inputCursor.current = e.target.selectionStart
if (e.target.type === 'text') {
inputRef.current = e.target
}
callback(e)
},
[callback],
)
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
}
})
return newCallback
}
export default useControlledInputOnChangeCursorFix
// Usage:
import React, { useReducer, useCallback } from 'react'
import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'
// Mimics this.setState for a class Component
const setStateReducer = (state, action) => ({ ...state, ...action })
const initialState = { street: '', address: '' }
const SomeComponent = props => {
const [state, setState] = useReducer(setStateReducer, initialState)
const handleOnChange = useControlledInputOnChangeCursorFix(
useCallback(({ target: { name, value } }) => {
setState({ [name]: value })
}, []),
)
const { street, address } = state
return (
<form>
<input name='street' value={street} onChange={handleOnChange} />
<input name='address' value={address} onChange={handleOnChange} />
</form>
)
}
For anybody having this issue in react-native-web here is solution written in TypeScript
const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => {
if(typeof refInput === "function") {
console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
return <TextInput key={"invalid-ref"} {...props} />;
}
if(!("HTMLInputElement" in self)) {
return <TextInput key={"no-web"} {...props} />;
}
const { value, onChange, ...restProps } = props;
const defaultRefObject = useRef<TextInput>(null);
const refObject: RefObject<TextInput> = refInput || defaultRefObject;
const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);
useEffect(() => {
if(refObject.current instanceof HTMLInputElement) {
refObject.current.setSelectionRange(selection.start, selection.end);
}
}, [ refObject, selection, value ]);
return (
<TextInput
ref={refObject}
value={value}
onChange={event => {
const eventTarget = event.target as any;
if(eventTarget instanceof HTMLInputElement) {
setSelection({
start: eventTarget.selectionStart,
end: eventTarget.selectionEnd
});
}
if(onChange) {
onChange(event);
}
}}
{...restProps}
/>
)
});
The simplest and safest way of doing this is probably to save the cursor position before React renders the input and then setting it again after React finishes rendering.
import React, {ReactElement, useEffect, useRef} from "react";
/**
* Text input that preserves cursor position during rendering.
*
* This will not preserve a selection.
*/
function TextInputWithStableCursor(
props: React.InputHTMLAttributes<HTMLInputElement> & {type?: "text"}
): ReactElement {
const inputRef = useRef<HTMLInputElement>(null);
// Save the cursor position before rendering
const cursorPosition = inputRef.current?.selectionStart;
// Set it to the same value after rendering
useEffect(function () {
if (
typeof cursorPosition === "number" &&
document.activeElement === inputRef.current
) {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
}
});
return <input ref={inputRef} {...props} />;
}
If you faced an issue with the cursor jumping to the end after updating the input state and updating the cursor using refs -> I found a workaround for it by setting the cursor in Promise.resolve's microtask.
<input
value={value}
onChange={handleValueUpdate}
ref={inputRef}
/>
const handleValueUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
// ...
// some value handling logic
setValue(newValue)
const cursorPosition = getCursorPositionLogic();
/**
* HACK: set the cursor on the next tick to make sure that the value is updated
* useTimeout with 0ms provides issues when two keys are pressed same time
*/
Promise.resolve().then(() => {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
});
}
I know the OP is 5 years old but some are still facing the same kind of issue and this page has an high visibility on Google search.
Try by replacing :
<input value={...}
with
<input defaultValue={...}
This will solve most of the cases i've seen around there.
I tried all of the above solutions and none of them worked for me. Instead I updated both the e.currentTarget.selectionStart & e.currentTarget.selectionEnd on the onKeyUp React synthetic event type. For example:
const [cursorState, updateCursorState] = useState({});
const [formState, updateFormState] = useState({ "email": "" });
const handleOnChange = (e) => {
// Update your state & cursor state in your onChange handler
updateCursorState(e.target.selectionStart);
updateFormState(e.target.value);
}
<input
name="email"
value={formState.email}
onChange={(e) => handleOnChange(e)}
onKeyUp={(e) => {
// You only need to update your select position in the onKeyUp handler:
e.currentTarget.selectionStart = cursorState.cursorPosition;
e.currentTarget.selectionEnd = cursorState.cursorPosition;
}}
/>
Also, be aware that selectionStart & selectionEnd getters are not available on input fields of type email.

Resources