I have an Input field to which if I enter a decimal value I want to round it off to its next highest number, but the problem is that whenever I type a dot (".") the onChnage gets triggered, rounds it off and then updates the value, so basically I'm not able to type anything after the dot ("."). I can't use onBlur here, can someone tell me what I can do
import React, { useState, ChangeEvent } from 'react';
import { TextInputHelperCurrency } from '#honeycomb-npm/honeycomb-react';
export type LoanAmountProps = {
label: string;
errorMessage: string;
defaultLoanAmount: string;
minLoanAmount: string;
maxLoanAmount: string;
};
const LoanAmount: React.VFC = () => {
const [enteredLoanAmount, setEnteredLoanAmount] = useState<string>('10000');
const handleAmountChange = (value: string) => {
const roundedUpValue = Math.ceil(Number(value));
setEnteredLoanAmount(String(roundedUpValue));
};
return (
<div className="loan-amount">
<TextInputHelperCurrency
data-testid="textInputHelperCurrency"
id="loanAmount"
value={enteredLoanAmount}
onChange={handleAmountChange}
/>
</div>
);
};
export default LoanAmount;
I haven't worked on ReactTS, but I will answer the solution in ReactJS. Feel free to edit the solution or suggest changes for TS.
Firstly, create a useRef hook which will be passed in the TextInputHelperCurrency as innerRef prop. Next, in useEffect hook, check if the useRef hook created above has loaded and if there is a click, and useRef hook does not contains the target event Node, then round up the value.
In the TextInputHelperCurrency component, accept innerRef as prop and attach the innerRef as ref to the parent tag of return statement. Here is the code:
const LoanAmount: React.VFC = () => {
const [enteredLoanAmount, setEnteredLoanAmount] = useState<string>('10000');
const textInputRef = useRef(null);
useEffect(() => {
const handleOutsideClickEvent = (event) => {
if (
textInputRef.current &&
!textInputRef.current.contains(event.target)
) {
const roundedUpValue = Math.ceil(Number(textInputRef.current.value));
setEnteredLoanAmount(String(roundedUpValue));
}
};
document.addEventListener('click', handleOutsideClickEvent);
return () => document.removeEventListener('click', handleOutsideClickEvent);
}, [textInputRef]);
return (
<div className='loan-amount'>
<TextInputHelperCurrency
data-testid='textInputHelperCurrency'
id='loanAmount'
value={enteredLoanAmount}
innerRef={textInputRef}
/>
</div>
);
};
Inside TextInputHelperCurrency component,
const TextInputHelperCurrency = ({innerRef}) => {
// some code
return (
<div ref={innerRef}>
{/* some more code */}
</div>
)
};
Related
On this StackBlitz project: https://stackblitz.com/edit/node-hxolmq?file=src%2Fmain.tsx
I have the following custom control...
/src/controls/IonInputMagic2.js
import { IonInput } from "#ionic/react";
import { useEffect, useState } from "react";
const IonInputMagic2 = props => {
const { value, onChange, validityFunc, ...others } = props
var isValidValue = validityFunc(value);
const initialValue = (typeof value !== 'undefined' && isValidValue) ? value : '';
const [ currentValue, setCurrentValue ] = useState(initialValue);
useEffect(() => {
setCurrentValue(initialValue);
}, [initialValue]);
const handleChange = (e) => {
var value = e.target.value;
if (!validityFunc(value)) {
e.preventDefault();
return false;
}
setCurrentValue(value);
if (onChange) {
onChange(e);
}
};
return (
<IonInput value={currentValue} onChange={handleChange} {...others} />
);
}
export default IonInputMagic2;
where you can see I use the Ionic control: IonInput.
My problem is: I have a validityFunc(...) that decides if what the user enters is acceptable or not. As per that function, only numeric and even digits are allowed. However, the user can enter whatever character with no restrictions.
I have a similar control: IonInputMagic1 which is very similar, but it uses the HTML built-in element: <input /> instead of the Ionic control: <IonInput />. On that control the user can only enter what is expected: only numeric and even digits.
Here is the difference between those 2 controls (left: works | right: doesn't work)
Here is how I use both controls:
What I need is: To make IonInputMagic2 (which uses: IonInput) work as: IonInputMagic1 where the user can only enter numeric and even digits. This is because the IonInput uses all the styling and scripting of Ionic and I don't want to break all that by using: <input />.
Note: I have detected through the DOM that the IonInput is a wrapper of: <input />.
Any idea on how to achieve that?
If possible, please fork the StackBlitz above and post the link here.
Thanks!
This change did the trick:
Here the full code for the component:
import { IonInput } from "#ionic/react";
import { useEffect, useState } from "react";
const IonInputMagic2 = props => {
const { value, onChange, validityFunc, ...others } = props
var isValidValue = validityFunc(value);
const initialValue = (typeof value !== 'undefined' && isValidValue) ? value : '';
const [currentValue, setCurrentValue] = useState(initialValue);
useEffect(() => {
setCurrentValue(initialValue);
}, [initialValue]);
const handleChange = (e) => {
var value = e.target.value;
if (!validityFunc(value)) {
e.preventDefault();
e.target.value = currentValue;
return false;
}
setCurrentValue(value);
if (onChange) {
onChange(e);
}
};
return (
<IonInput value={currentValue} onIonInput={handleChange} {...others} />
);
}
export default IonInputMagic2;
I lost some hours and don't get this. I tried a ton of examples, but nothing worked. How do I solve these sort of challenges? How do I know what kind of types Typescript wants for those things, and why the hell should ANYONE do such things with typescript, since in JS it just works!
I just want to pass my useState from parent to the child, and there I want to set a Ref of a div to the useState so that the parent can manipulate the div (and its children).
const Parent: React.FC<iParent> = ({title}) => {
const x = React.useRef() as React.MutableRefObject<HTMLDivElement>;;
const refSetter = useState(x);
return(
<Child setMyRef={refSetter} />
)
}
interface iChild {
setMyRef: Dispatch<SetStateAction<React.MutableRefObject<HTMLDivElement>>>;
}
const Child: React.FC<iChild> = ({setMyRef}) => {
const myRef = useRef(???type?);
useEffect(() => {
if(myRef.current){
setMyRef(myRef);
}
}, [])
return(
<div ref={myRef} />
)
}
You can do it as below. A way to know what type for what variable is to read the error you get and also what you see when you hover over one.
import {Dispatch, FC, RefObject, SetStateAction, useEffect, useRef, useState } from "react";
interface iChild {
setMyRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
}
const Child: FC<iChild> = ({ setMyRef }) => {
const myRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setMyRef(myRef);
}, []);
return <div ref={myRef} />;
};
const Parent: FC<iParent> = ({ title }) => {
const [ref, refSetter] = useState<RefObject<HTMLDivElement> | null>(null);
useEffect(() => {
console.log(ref?.current);
}, [ref, ref?.current]);
return <Child setMyRef={refSetter} />;
};
Having the following Textarea component, it was built in order to be reusable, it is a basic textarea with has a maxlength props, where it can be specified the maximum input length, it also shows the current input length, in the format current input length/max length.
It works fine as a separate component, the problem is when it must be used with react-hook-form, the current input length isn't updating.
Here is the Textarea component:
import React, { useState, useEffect } from 'react';
import useTextareaController from './use-textarea-controller';
export interface TexareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
maxLength?: number;
id: string;
}
export const Textarea = React.forwardRef(
(
{ id, maxLength = 200, ...props }: TexareaProps,
ref: React.ForwardedRef<HTMLTextAreaElement>
) => {
const { textareaRefSetter } = useTextareaController(ref);
const [count, setCount] = useState(0);
useEffect(() => {
const refElement = document.getElementById(id) as HTMLTextAreaElement;
if (refElement) {
setCount(refElement.value.length);
}
}, [id]);
return (
<div>
<div>
{count}/{maxLength}
</div>
<textarea
id={id}
ref={textareaRefSetter}
onChange={(event) => setCount(event.target.value.length)}
maxLength={maxLength}
{...props}
></textarea>
</div>
);
}
);
export default Textarea;
it used useTextareaController from another hook, here it is the code:
import React, { useRef } from 'react';
/**
* Utility function which registers and then removes event listeners for specified elements.
* #param el Reference of element for which to register event
* #param eventType native event type
* #param onEventCallback callback to be bound to event
*/
/**
* Controls the appearance of Button / Icon that is assigned to the reference,
* using the visibility property.
*
* This implementation has been made in the effort of
* keeping the input uncontrolled whenever it is possible.
*
* #See https://react-hook-form.com/api/useform/register/
* #param forwardedInputRef forwarded reference to be set by this hook
* #param disabled clear button / icon won't appear if it is false
* #returns referenceSetter function to assign the inner input element to the forwarded reference
* and the reference of the clear button / icon
*/
export const useTextareaController = (
forwardedInputRef: React.ForwardedRef<HTMLTextAreaElement>
) => {
const innerInputRef = useRef<HTMLTextAreaElement>();
// Both the inner reference and the forwarded reference should be set
const textareaRefSetter = (el: HTMLTextAreaElement) => {
innerInputRef.current = el;
if (!forwardedInputRef) return;
if (typeof forwardedInputRef === 'function') {
forwardedInputRef(el);
}
if (typeof forwardedInputRef === 'object') {
forwardedInputRef.current = el;
}
};
return { textareaRefSetter };
};
export default useTextareaController;
Here is a modal component that has Textarea inside of it and uses react-hook-form for validation:
import { yupResolver } from '#hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import { Modal, Textarea } from '../shared/ui-components';
const schema = yup.object().shape({
description: yup.string().required()
});
export interface Task {
description: string;
}
export interface MyModalProps {
title: string;
open: boolean;
toggle: () => void;
}
export function MyModal({ title, open, toggle }: MyModalProps) {
const emptyTask = { description: '' };
const { handleSubmit, reset, register } = useForm({
resolver: yupResolver(schema)
});
const onSubmit = (data: Task) => {
// send a POST request
toggle();
reset(emptyTask);
};
return (
<Modal title={title} open={open} onClose={toggle} onSubmit={handleSubmit(onSubmit)}>
<div>
<Textarea id='my-textarea' {...register('description')} />
</div>
</Modal>
);
}
export default MyModal;
Is there a way to make the current input length to work in combination with react-hook-form?
I guess the changes must be done in Textarea component.
react-hook-form provides its own onChange handler which it'll pass as a part of props, which is likely clobbering your custom handler when you spread props into the textarea props.
You should instead extract onChange from props and define your own onChange callback which invokes it if it were passed in, rather than spreading it into your props.
export const Textarea = React.forwardRef(
(
{ id, maxLength = 200, onChange, ...props }: TexareaProps,
ref: React.ForwardedRef<HTMLTextAreaElement>
) => {
const { textareaRefSetter } = useTextareaController(ref);
const [count, setCount] = useState(0);
useEffect(() => {
const refElement = document.getElementById(id) as HTMLTextAreaElement;
if (refElement) {
setCount(refElement.value.length);
}
}, [id]);
const onChangeHandler = useCallback(
(event) => {
setCount(event.target.value.length);
onChange?.(event);
},
[setCount, onChange]
);
return (
<div>
<div>
{count}/{maxLength}
</div>
<textarea
id={id}
ref={textareaRefSetter}
onChange={onChangeHandler}
maxLength={maxLength}
{...props}
></textarea>
</div>
);
}
);
I'm working with controlled input elements at work and I'm stuck.
Basically, I need to autofill some input elements in a form, but the problem is that I need to fill it in a way that simulates the user input (in this case, typing) in order to trigger the onChange function's logic. So, because of that. I need to emulate the typing behavior and not just set the value for the element.
Despite having searched for previous questions and reading docs about KeyboardEvent, I haven't been able to make this work.
Currently, I'm experimenting in a Codesandbox just for making things easier, but even with this simple environment, I can't manage to get this to work.
Here's the code and its Codesandbox link
import { useRef, useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState();
const inputRef = useRef();
const event = new KeyboardEvent("keypress", { key: 99 });
useEffect(() => {
inputRef.current.dispatchEvent(event);
}, [inputRef]);
const onChange = (e) => {
setState(e.target.value);
};
return (
<div className="App">
<h1>{state}</h1>
<input
type="text"
id="name"
onChange={onChange}
ref={inputRef}
value={state}
/>
</div>
);
}
Hopefully one of you guys could give me a hand with this.
Thanks for reading!
Related to the comments:
I think that it shouldn't be necessary to be dispatching a keypress event to get your special effect logic to run.
For example, you can use a useEffect which just runs on initial render to trigger whatever special logic you want -- and this way you can just have a regular initial value for the form state.
import { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
// In the useState call, you can initialize the value.
const [state, setState] = useState("initial value");
const specialEffectFunction = () => {
// here's the code for the special effect you want to run on load
console.log('this is the special onChange effect')
}
useEffect(() => {
// This will trigger the special function which you want to run
// when the app loads
specialEffectFunction();
// if it really HAS to be the `onChange` function that's called,
// then you'll need to call that with a fake ChangeEvent.. but I don't
// think that should be necessary...
}, [])
const onChange = (e) => {
setState(e.target.value);
};
return (
<div className="App">
<h1>{state}</h1>
<input
type="text"
id="name"
onChange={onChange}
value={state}
/>
</div>
);
}
I couldn't fix the problem with Keyboard Event for my lack of knowledge about it, but I hope I managed to solve the problem of emulating a human autofill the input using the below code.
function AutoFillInput({ finalValue }: { finalValue: string }) {
const [inputValue, setInputValue] = useState('');
const [sliceStart, setSliceStart] = useState(0);
const changeHandler = useCallback((event) => {
setInputValue(event.target.value);
}, []);
useEffect(function handleFinalValueChange() {
setInputValue('');
if (sliceStart < finalValue.length)
setSliceStart(x => x + 1);
}, [finalValue]);
useEffect(function handleSlice() {
setInputValue(finalValue.slice(0, sliceStart));
if (sliceStart < finalValue.length) {
setTimeout(() => {
setSliceStart(x => x + 1);
}, 800);
}
}, [sliceStart]);
return (
<input
value={inputValue}
onChange={changeHandler}
placeholder={'Auto fill input'}
/>
)
}
function App() {
return (
<div >
<AutoFillInput finalValue={'hello world'} />
</div>
);
}
export default App;
I'm new to React and wondering how to change this code so that I'm not using any for the add function that is DI'd into the component.
Most of what I read says to use the React mouse click event type but that has only 1 param and isn't really what is going on anyway so seems bad two different ways.
import React, { useState } from 'react';
interface IProps {
count?: number;
incrementBy?: number;
onClick: any;
// EDIT - FIX - correct fn type
// I also took optional ? off types in app
//onClick: (count: number, incrementBy: number) => void;
}
const Description2: React.FC<IProps> = (props: IProps) => (
<div>
<p>My favorite number is {props.count}, incrementBying by {props.incrementBy}</p>
<button
onClick={() => props.onClick(props.count, props.incrementBy)}
>
Increase
</button>
</div>
);
const App: React.FC = () => {
//initialize state
const increase = 4;
const [count, setCount] = useState(increase);
const [user, setUser] = useState("world");
const add = (currentCount: number, bump: number) => {
setCount(currentCount + bump);
};
return (
<div >
<Description2
count={count}
incrementBy={increase}
onClick={add} />
</div>
);
}
export default App;
The correct type would be:
(count: number, incrementBy: number) => any