React: How to get a group value of components? - javascript

I have a input component that does validation. In a reduced form it looks like this:
const InputField = ({validation, name}) => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
let errors = 0;
if (validation.includes("required") && val === "") {
errors++;
}
if (errors > 0) {
setError(true);
} else {
setError(false);
}
setValue(e.target.value)
}
return (
<input
type="text"
value={value}
onChange={handleChange}
name={name}
/>
);
};
If I am using this component multiple times in a parent like
const Parnet = () => {
// 🔥🔥🔥 help required here
const groupedErrorValueBool = true;
return (
<>
<InputField name="Name" validation="required" />
<InputField name="Email" validation="required" />
<InputField name="Birthday" validation="required" />
<button disabled={groupedErrorValueBool}>Submit</button>
</>
);
How can I get a grouped value of errors? Like: I want to disable a submit button, if any field has any error.
Ideally I know which component has an error, so that I can print meaningful comments.

Implementation
You could do it like this:
const InputField = ({validation, onErrorChanged}) => {
const [value, setValue] = React.useState("test");
const [error, setError] = React.useState(false);
const handleChange = (e) => {
let errors = 0;
if (validation.includes("required") && e.target.value === "") {
errors++;
}
if (errors > 0 && !error) {
// we have error(s) and the error state is set to false
setError(true);
onErrorChanged(true);
} else if (errors === 0 && error){
// we don't have any error but the error state is set to true
setError(false);
onErrorChanged(false);
}
setValue(e.target.value)
}
return (
<input
type="text"
value={value}
onChange={handleChange}
/>
);
};
const InputGroup = () => {
const [errors, setErrors] = React.useState(0);
function errorChanged(hasError){
if(hasError) setErrors(errors + 1);
else setErrors(errors - 1);
}
return (
<React.Fragment>
<InputField validation="required" onErrorChanged={errorChanged}/>
<InputField validation="required" onErrorChanged={errorChanged}/>
<InputField validation="required" onErrorChanged={errorChanged}/>
<button disabled={errors === 0 ? false: true}>Submit</button>
</React.Fragment>
);
}
ReactDOM.render(<InputGroup/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Explanation
Child component
You need to pass a callback function to the children that is called whenever the error state changes on a child element i. e.
when we have found an error in the input and error state is currently set to false
when we have no error(s) in the input and error state is currently set to true
Parent component
In the parent component we need a state errors which is a counter that counts the number of child component that have errors. Additionally we need to define the above mentioned callback function such that it increments the counter whenever a child component reports it has an error and decrements the counter when a child component reports it has switched to state no error.
If the counter equals zero we know we don't have any error in any of our child components, if it's not null we know how many child components have errors.

The groupedErrorValueBool state would need to be in your parent component then, then you give the state handler to the children
const InputField = ({ validation, setGroupErrorBool }) => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
let errors = 0;
if (validation.includes("required") && val === "") {
errors++;
}
if (errors > 0) {
setError(true);
setGroupErrorBool(true);
} else {
setError(false);
}
setValue(e.target.value);
};
return <input type="text" value={value} onChange={handleChange} />;
};
const Parent = () => {
const [groupErrorBool, setGroupErrorBool] = useState(false);
return (
<>
<InputField validation="required" setGroupErrorBool={setGroupErrorBool} />
<InputField validation="required" setGroupErrorBool={setGroupErrorBool} />
<button disabled={groupErrorBool}>Submit</button>
</>
);
};
Form stuff is admittedly can be very difficult. That's why I use "Formik" for my React forms. Formik I believe will use a parent/overarching form control element with rendered props technique to expose form state handlers, which could also be something you can try yourself

Related

How to save data from different children to a single array in parent component

Here I have three components. Whenever I click the button in any of the child components (First and Second) the parent (Test) component should display text entered into the child component. The problem is that whenever I click the button of other child component a new array is created with data from that particular component.
Here's the code:
Test component:
function Test() {
const [ChildData , setChildData] = useState('');
console.log(ChildData)
return (
<div>
<h1>Test</h1>
<div>
{ChildData? <div>{ChildData.map((data)=>{return <div>{data}</div> })}</div>:null }
</div>
<First passChildData={setChildData}/>
<Second passChildData={setChildData}/>
</div>
)
}
First component:
function First(props) {
const [Data, setData] = useState('');
const [Items, setItems] = useState('')
const handlechange = (e)=>{
setData(e.target.value)
}
const handleClick = ()=>{
if(Data != ''){
setItems((prevData)=>{
const newData = [...prevData,Data];
props.passChildData(newData)
return newData
})
setData('')
}else{
console.log("enter something")
}
}
return (
<div>
<h1>First</h1>
<input type="text" onChange={handlechange} value={Data}/>
<button onClick={handleClick}>Add</button>
{Data}
<div>{Items? <div>{Items.map((item)=>{return <div>{item}</div> })}</div>:null }</div>
</div>
)
}
Second component:
function Second(props) {
const [Data, setData] = useState('');
const [Items, setItems] = useState('')
const handlechange = (e)=>{
setData(e.target.value)
}
const handleClick = ()=>{
if(Data != ''){
setItems((prevData)=>{
const newData = [...prevData,Data];
props.passChildData(newData)
return newData
})
setData('')
}else{
console.log("enter something")
}
}
return (
<div>
<h1>Second</h1>
<input type="text" onChange={handlechange} value={Data}/>
<button onClick={handleClick}>Add</button>
{Data}
<div>{Items? <div>{Items.map((item)=>{return <div>{item}</div> })}</div>:null }</div>
</div>
)
}
What I expect is:
When I enter "What" into the First component;
Then "is" into the Second component;
Then "your" into the First component
Then "name?" in Second component
I want "What is your name?" (Doesn't matter if it's in different divs or lines; I want every single item to be displayed in the order it was entered) to be shown inside the parent (Test) component.
gracias ; ).....
First, I would not have two components - First and Second - that are nearly identical except for their headings; I would have a single component with heading as a prop.
Second, I would use a <form> with an onSubmit handler instead of a <button> with an onClick handler because this will allow the user to add an item by, for example, pressing [Enter] while the input field has focus.
Third, I would use the useEffect hook to listen for changes in the First component to the Items array and then call the passChildData prop function when there is a change. Note that variables that are to reference arrays should be initialized with empty arrays, for example: const [Items, setItems] = useState([]).
The final code looks like:
function Test() {
const [ChildData, setChildData] = useState([]);
const setItem = (newItem) => {
setChildData([...ChildData, newItem]);
}
return (
<div>
<h1>Test</h1>
<div>
{ChildData.map((data, index) => <div key={index}>{data}</div>)}
</div>
<First heading="First" passChildData={setItem}/>
<First heading="Second" passChildData={setItem}/>
</div>
)
}
function First(props) {
const [Data, setData] = useState('');
const [Items, setItems] = useState([])
useEffect(() => {
props.passChildData(Data);
}, [Items])
const handlechange = (e)=>{
setData(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault();
if (Data === '') {
console.log('enter something');
} else {
setItems([...Items, Data]);
}
}
return (
<div>
<h1>{props.heading}</h1>
<form onSubmit={handleSubmit}>
<input type="text" onChange={handlechange} value={Data}/>
<button type="submit">Add</button>
{Data}
</form>
<div>
{Items.map((item, index) => <div key={index}>{item}</div>)}
</div>
</div>
)
}
I have created a fiddle for reference.
The problem is that you are mutating the whole state within every component. In other words, you don't use prevData.
I would change the state in onClick method of First and Second component, like this:
setItems((prevData)=>{
const newData = [***...prevData***, ...Items,Data];
props.passChildData(newData)
return newData
})

React Native - react hook form - setValue not re-render Controller component

Re-render problem
I have two components:
MainForm
CFNumericInput
The values coming from CFNumericInput are correct, but setValue won't render the old one.
Do I have to use an useEffect?
MainForm
const { control, watch, setValue, register } = useFormContext();
return (
<CFNumericInput
name="number1"
control={control}
setValueOnChange={(nextValue: string, oldValue: string) => {
let nextValidValue = checkNumericType(nextValue, "f6.2");
if (nextValidValue !== "") setValue("number1", nextValidValue);
else if (oldValue) setValue("number1", oldValue);
}}
/>;
)
CFNumericInput
export const CFNumericInput: React.FC<any> = ({
name,
control,
setValueOnChange,
}) => {
return control ? (
<Controller
name={name}
control={control}
render={({ field }) => {
return (
<NumericInput
{...field} // onChange, onBlur, value, name, ref
title={title}
onChange={(e) => {
field.onChange(e);
setValueOnChange && setValueOnChange(e, field.value);
}}
/>
);
}}
/>
) : (
<></>
);
};
Working but heavy solution
This solution it's working, but it's really heavy.
const [number1] = watch(["number1"]);
const [old, setOld] = useState("");
useEffect(() => {
let nextValidValue = checkNumericType(number1, "f6.2");
if (nextValidValue !== "") {
setValue("number1", nextValidValue);
setOld(nextValidValue);
} else if (old) setValue("number1", old);
}, [number1]);
Isn't possible that you call checkNumericType on onSubmit?
Also you can try on looking to use Yup (take a look at: https://react-hook-form.com/advanced-usage/#CustomHookwithResolver)
let me know what have you tried

React function prop gives "TypeError is not a function"

I am new to react and I'm trying to pass a function as a prop to a child component.
In my case this is the parent component:
export default function Game() {
const [gameStarted, setGameStarted] = useState(false)
const [gameSettings, setGameSettings] = useState({})
useEffect(() => {
//setGameStarted(true);
}, [gameSettings]
)
return (
<>
{!gameStarted &&
<div className="game-form">
<GameSelection handleGameSelection={(settings)=> setGameSettings(settings)}/>
</div>}
</>
)}
My child component is:
export default function GameSelection({handleGameSelection}) {
const [labels, setLabels] = useState([])
const [gameMode, setGameMode] = useState('')
const [selectedLabels, setSelectedLabels] = useState([])
const [formError, setFormError] = useState(null)
// create label values for react-select
useEffect(() => {
if(document) {
setLabels(document.cards.map(card => {
return { value: {...card}, label: card.label}
}))
}
}, [document])
const handleSubmit = (e) => {
e.preventDefault()
try{
const gameSettings = {
mode: gameMode.value,
selected: selectedLabels.map((card) => ({...card.value})),
}
handleGameSelection(gameSettings)
}
catch(error){
console.log(error)
}
}
return (
<>
<h2 className="page-title">Please select your game</h2>
<form onSubmit={handleSubmit}>
<label>
<span>Mode:</span>
<Select
onChange={(option) => setGameMode(option)}
options={gameModes}
/>
</label>
<label>
<span>Select labels:</span>
<Select
onChange={(option) => setSelectedLabels(option)}
options={labels}
isMulti
/>
</label>
<button className="btn" >Start game</button>
{formError && <p className="error">{formError}</p>}
</form>
</>
)}
My form works but when I submit the form I keep getting the error TypeError: handleGameSelection is not a function. I tried everything. I have created a separate function in the parent component and gave that as a prop to the child. That also didn't work. I don't know what I am doing wrong. Any ideas?
Run this function inside useEffect, because currently, you are running this function before component is fully mounted and this function is propably undefined... or you can try to use if(typeof handleGameSelection === 'function') to check if its already initialized

How to get each user's keystroke when he pressed a certain key?

I need to get each user's keystroke when he pressed a certain key("#") and stop getting his keystroke when he pressed other key(space(" ")). For example: a user enters the text "I wanna go to #shop", I need to save his input and the tag inside it. How can I do it? I wrote some code to do it but I don't know how to make it completely
onKeyDown = (e) => {
let value = e.target.value, tags = [], currentTag = "";
if (e.key == "Enter") {
this.setState((state) => {
const item = this.createNote(value, tags);
return { notes: [...state.notes, item] };
});
}
if (e.key == "#") {}
};
You can make use of regex /#[^\s]+/g
Live Demo
export default function App() {
const [value, setValue] = useState("");
const [tags, setTags] = useState([]);
function onInputChange(e) {
const value = e.target.value;
setValue(value);
const tags = value.match(/#[^\s]+/g) ?? [];
setTags(tags);
}
return (
<>
<input type="text" name="" value={value} onChange={onInputChange} />
<ul>
{tags.map((tag) => {
return <li key={tag}> {tag} </li>;
})}
</ul>
</>
);
}
EDITED: You can make use of useMemo hook as
Thanks to 3limin4t0r
Live Demo
export default function App() {
const [value, setValue] = useState("");
const tags = useMemo(() => value.match(/#\S+/g) || [], [value]);
function onInputChange(e) {
const value = e.target.value;
setValue(value);
}
return (
<>
<input type="text" name="" value={value} onChange={onInputChange} />
<ul>
{tags.map((tag) => {
return <li key={tag}> {tag} </li>;
})}
</ul>
</>
);
}
Instead of parsing individual key values, you can use a function like this to parse your input field on changes and return an array of hashtags (without the leading #):
TS Playground link
function parseTags (input: string): string[] {
return (input.match(/(?:^#|[\s]#)[^\s]+/gu) ?? []).map(s => s.trim().slice(1));
}
Here's a working example in a functional component which incorporates the function:
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.16.4/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="react">
const {useState} = React;
function parseTags (input) {
return (input.match(/(?:^#|[\s]#)[^\s]+/gu) ?? []).map(s => s.trim().slice(1));
}
function Example () {
const [value, setValue] = useState('');
const [tags, setTags] = useState([]);
const handleChange = (ev) => {
const {value} = ev.target;
setValue(value);
setTags(parseTags(value));
};
return (
<div>
<input
type="text"
onChange={handleChange}
placeholder="Type here"
value={value}
/>
<div>Parsed tags:</div>
<ol>
{tags.map((str, index) => <li key={`${index}.${str}`}>{str}</li>)}
</ol>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
Something like this should work for you; You can adapt if you don't have access to hooks:
const RecorderInput = ({ onChange }) => {
const [isRecording, setIsRecording] = useState(false);
const toggleRecording = (e) => {
const character = String.fromCharCode(e.charCode);
if (character === '#') {
setIsRecording(true);
}
if (character === ' ') {
setIsRecording(false);
}
}
const handleChange = (e) => {
if (isRecording) onChange(e);
toggleRecording(e);
}
<input type="text" onChange={handleChange} />
}
As other suggested your onChange can also use regex groups to capture hashes as the user types. Thinking about this now, it would probably be a lot cleaner to do it this way but regex is well documented so I won't go through the hassle

How to create an event similar to onChange in React

I have a react native input component that takes a regex and emit a boolean if the regex matches.
I wanted to do the similar thing but return a string value in React Js but the problem is I don't totally understand how the react native input component is working.
Custom Input component
class CustomInput extends Component {
handleValidation(value) {
const {pattern} = this.props;
if (!pattern) {
return true;
}
// string pattern, one validation rule
if (typeof pattern === 'string') {
const condition = new RegExp(pattern, 'g');
return condition.test(value);
}
// array patterns, multiple validation rules
if (typeof pattern === 'object') {
const conditions = pattern.map(rule => new RegExp(rule, 'g'));
return conditions.map(condition => condition.test(value));
}
}
onChange(value) {
const {onChangeText, onValidation} = this.props;
const isValid = this.handleValidation(value);
onValidation && onValidation(isValid);
onChangeText && onChangeText(value);
}
render() {
const {pattern, onChangeText, children, style, ...props} = this.props;
return (
<Input
style={style}
onChangeText={value => this.onChange(value)}
{...props}
autoCapitalize="none">
{children}
</Input>
);
}
}
Usage
<CustomInput
value={pass} //state variable
onChangeText={onChangePass} //state variable setter
pattern={[regexes.password]}
onValidation={isValid => performValidation(isValid)}
//performValidatio method enables and disables a button according to isValid
/>
Now I want to make a component in ReactJs that has an Input and other elements the input will take a regex and and it will return the value of its input to the parent. It will be used in a form that will have many inputs and all inputs need to have and error msg and validations.
You can create custom component with controlled state. You need to set local state within the custom component. On change of value, you can parse and validate. after that set it to local state. You can leverage error either local of delegate to parent. Here below sample, i have delegated to parent on validation.
const Input = ({
onChangeText,
pattern,
onValidation,
defaultValue,
error,
...rest
}) => {
const [value, setValue] = useValue(defaultValue);
const onChange = ({ target: { value } }) => {
if (pattern) onValidation(pattern.test(value));
setValue(value);
onChangeText(value);
};
return (
<div className="wrapper">
<input value={value} onChange={onChange} {...rest} />
{error && <span>{error}</span>}
</div>
);
};
I have understood the logic if anyone wants to make an input like this they can use this code
I have used react-bootstrap for styling
The Custom Input
import React, { useState } from "react";
export default function CustomInput(props) {
const { error, inputProps, regex, className, onInput } = props;
const [showError, setShowError] = useState(false);
const [text, setText] = useState("");
const handleChange = (val) => {
setText(val);
if (regex.test(val)) {
setShowError(false);
} else {
setShowError(true);
}
onInput && onInput(val);
};
return (
<div className={className}>
<input
value={text}
className={showError ? "form-control border-danger" : "form-control"}
{...inputProps}
onChange={(e) => handleChange(e.target.value)}
/>
{showError && error ? (
<small className="text-danger">{error}</small>
) : null}
</div>
);
}
And use it in a parent component like this
const [temp, setTemp] = useState("");
<CustomInput
className="form-group col-md-3"
inputProps={{ placeholder: "test", maxLength: "50" }}
error="Required"
regex={regexes.url}
onInput={(val) => setTemp(val)}
/>
Somebody please confirm if this is a good approach

Categories