I am working on a react where I am dealing with dynamic number of input fields as I am rendering them using arr.map function. But How can I handle the input onChange method with so many input fields?
Here's my component:
this.props.setsList.map((codeset, index) => (
<Table.Row key={codeset.code_system_id}>
<Table.Cell>{index + 1}</Table.Cell>
<Table.Cell>{codeset.name}</Table.Cell>
<Table.Cell>
<Input
type='text'
className='form-control'
value={codeset.code}
placeholder={translateText('Code')}
onChange={() => this.handleCodeChange(event, index)}
style={{ height: '70%' }}
/>
</Table.Cell>
<Table.Cell>
<Input
type='text'
className='form-control'
value={codeset.description}
placeholder={translateText('Code Description')}
onChange={() => this.handleCodeDescriptionChange(event, index)}
style={{ height: '70%' }}
/>
</Table.Cell>
</Table.Row>
All the input fields may have existing fields or may be empty and one can edit it them semd the edit fields to the API. How can I handle such a case with just 1 handleFunction. Is there a way? Any leads will be appreciated.
In the above code, 2 input boxes are there with initial values from the api.
Data passed into your component should go directly into state. Then each field sends the array index, field name and new value to the handleChange callback.
import { useState } from "react";
// passed in as a prop from the parent component
const data = [
{
name: "foo",
code: "gdfgsd"
},
{
name: "bar",
code: "gfdsgsdfgfd"
}
];
const App = ({ setsList = data }) => {
const [state, setState] = useState(setsList);
const handleChange = (e, i) => {
const { value, name } = e.target;
const newState = [...state];
newState[i] = {
...newState[i],
[name]: value
};
console.log(newState);
setState(newState);
};
return (
<div className="App">
{state.map(({ name, code }, index) => {
return (
<div key={index}>
<label>
name
{": "}
<input
name="name"
value={name}
onChange={(e) => handleChange(e, index)}
/>
</label>
<label>
code
{": "}
<input
name="code"
value={code}
onChange={(e) => handleChange(e, index)}
/>
</label>
</div>
);
})}
</div>
);
};
export default App;
There's also a way that you can render each field dynamically by mapping over the array, and then inner mapping over that array item's keys.
import { useState } from "react";
import { data } from "./data";
const App = ({ setsList = data }) => {
const [state, setState] = useState(setsList);
const handleChange = (e, i) => {
const { value, name } = e.target;
const newState = [...state];
newState[i] = {
...newState[i],
[name]: value
};
console.log(newState);
setState(newState);
};
return (
<table className="App">
<tbody>
{state.map((item, index) => (
<tr key={index}>
{Object.keys(item).map((key) => (
<td key={`${index}-${key}`}>
<label>
{key}
{": "}
<input
name={key}
value={item[key]}
onChange={(e) => handleChange(e, index)}
/>
</label>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default App;
Related
I'm doing application with friends. I'm using react-hook-form. The api gets only null values. It's a very crucial element of our application. Please help, here is my code:
const NewItemBar = ({ isVisible, handleClose }) => {
const { register, handleSubmit, watch, errors } = useForm();
const onSubmit = ({ name, data, description }) => {
api
.post(endpoints.createTask, {
name,
data,
description,
})
.then((response) => {
console.log(response);
})
.catch((err) => console.log(err));
};
return (
<StyledWrapper handleClose={handleClose} isVisible={isVisible}>
<StyledHeading big>Dodaj zadanie do wykonania</StyledHeading>
<StyledForm onSubmit={handleSubmit(onSubmit)}>
<StyledInput
placeholder="nazwa zadania"
type="text"
name="name"
{...register('name', { required: 'Required' })}
/>
<StyledInput
placeholder="data wykonania"
type="text"
name="data"
{...register('data', { required: 'Required' })}
/>
<StyledTextArea
type="text"
placeholder="opis"
name="description"
as="textarea"
{...register('description', { required: 'Required' })}
/>
<StyledButton type="submit">Zapisz</StyledButton>
</StyledForm>
</StyledWrapper>
);
};
In case of custom components you should create wrapper using Controller or pass a function with forwardRef.
https://react-hook-form.com/get-started#IntegratingwithUIlibraries
forwardRef might be useful when you want your component to manage inner state on it's own.
For example:
const PhoneInput = forwardRef(
(
{
id,
name,
label,
placeholder,
errorMsg,
onSubmit,
onChange,
onBlur,
disabled,
},
ref
) => {
const [value, setValue] = useState("");
const _onChange = (value) => {
setValue(value);
onChange(value);
};
const classes = (className) =>
[
className,
disabled ? "disabled" : "",
errorMsg ? "is-invalid" : "",
].join(" ");
console.log(value);
return (
<div className={classes("f-form-group phone-input")}>
<div className="phone-input-wrap">
<div className="inputs">
<label htmlFor={name}>{label}</label>
<NumberFormat
className={classes("f-form-control")}
name={name}
id={id}
type="tel"
format="+7 (###) ### ##-##"
mask="_"
placeholder={placeholder}
value={value}
onChange={(e) => _onChange(e.target.value)}
onBlur={onBlur}
getInputRef={ref}
/>
</div>
<button
type="button"
className="btn btn-primary"
onClick={() => onSubmit(value)}
>
Подтвердить
</button>
</div>
<div className="invalid-feedback">{errorMsg}</div>
</div>
);
}
);
I want to get an attribute from a function within a component. The function is called checkbox.js:
export default function Checkboxes() {
const [checked, setChecked] = React.useState(false);
const handleChange = (event) => {
setChecked(event.target.checked);
};
return (
<div>
<Checkbox
checked={checked}
onChange={handleChange}
color="primary"
inputProps={{ 'aria-label': 'primary checkbox' }}
/>
</div>
);
}
The component Checkbox multiple times in a table row. props.characterData is an array that has various attributes.
const TableBody = props => {
const rows = props.characterData.map((row, index) => {
return (
<tr key={index}>
<td>
<Checkbox />
</td>
<td>{row.task}</td>
<td>{row.desc}</td>
</tr>
)
})
return <tbody>{rows}</tbody>
}
What I want to do is to save the "checked" boolean value into the row.checked attribute every time it is changed, but nothing I have tried or searched up worked.
The row.checked comes from an array of characters where each character is initialized in the form:
{ task: '', desc: '', checked: false}
In the TableBody, it is mapped for each element in the array.
Any help would be greatly appreciated.
import React, { useState, useEffect } from 'react'
const Checkboxes =({checked, cbOnChange})=> {
const handleChange = (event) => {
cbOnChange(event.target.checked);
};
return (
<div>
<Checkbox
checked={checked}
onChange={handleChange}
color="primary"
inputProps={{ 'aria-label': 'primary checkbox' }}
/>
</div>
);
}
const TableBody = props => {
const [checkboxValue, setCheckboxValue] = useState({})
useEffect(() => {
setCheckboxValue({})
}, [JSON.stringify(props.characterData)])
const handleChange =(index, checked)=>{
setCheckboxValue((state)=>({
...state,
[index]: checked
}))
}
const rows = props.characterData.map((row, index) => {
return (
<tr key={index}>
<td>
<Checkboxes cbOnChange={(checked)=> handleChange(index, checked)} checked={checkboxValue[index] || false}/>
</td>
<td>{row.task}</td>
<td>{row.desc}</td>
</tr>
)
})
return <tbody>{rows}</tbody>
}
export default TableBody
you should write like this:
export default function App() {
const [characterData , setCharacterData] = React.useState([{
id: 1,
priority : true,
task: "task1",
desc: "desc1"
},
{
id: 2,
priority : false,
task: "task2",
desc: "desc2"
}])
const handleChangePriority = (id , value) => {
let cloneCharacterData = [...characterData]
const selectData = characterData.filter(itm => itm.id === id)[0];
const index = cloneCharacterData.findIndex(itm => itm.id === id)
selectData.priority = value;
cloneCharacterData[index] = selectData;
setCharacterData(cloneCharacterData)
}
return (
<div >
<TableBody characterData={characterData} handleChangeProps={handleChangePriority} />
</div>
);
}
TableBody:
const TableBody = props => {
const rows = props.characterData.map((row, index) => {
return (
<tr key={index}>
<td>
<Checkbox id={row.id} priority={row.priority} handleChangeProps={props.handleChangeProps} />
</td>
<td>{row.task}</td>
<td>{row.desc}</td>
</tr>
)
})
return <tbody>{rows}</tbody>
}
And Checkboxes:
export default function Checkboxes({priority , handleChangeProps , id}) {
// const [checked, setChecked] = React.useState(false);
const handleChange = event => {
// setChecked(event.target.checked);
handleChangeProps(id , event.target.checked)
};
return (
<div>
<Checkbox
checked={priority}
onChange={handleChange}
color="primary"
inputProps={{ "aria-label": "primary checkbox" }}
/>
</div>
);
}
I have a dynamic array of form fields, whose values are fetched via REST API. On the page, there is also a dropdown, that, when changed, shows a different array of fields. I fetch all of these fields/values during the componentDidMount life cycle hook and filter the list to show the relevant data.
The Formik docs mention FieldArrays as a means to handle an array of fields. However, their example shows a static list of objects as its initialValues -- but I don't see how dynamically generated lists. In fact, since I'm fetching initialValues via AJAX, it's initially an empty array -- so nothing is rendered even after getting the data.
This is simplified version of my code:
const MyComponent = class extends Component {
componentDidMount() {
// data structure: [{Name: '', Id: '', Foo: '', Bar: ''}, ...]
axios
.get('/user')
.then((res) => {
this.setState({
userData: res.data
});
});
}
render() {
return (
<div>
<Formik
initialValues={{
users: this.state.userData
}}
render={({values}) => (
<Form>
<FieldArray
name="users"
render={arrayHelpers => (
<ul>
{
values.users.map((user, index) => {
return (
<li key={user.Id}>
<div>{user.Name}</div>
<Field name={`user[${index}].Foo`} type="text" defaultValue={user.Foo} />
<Field name={`user[${index}].Bar`} type="text" defaultValue={user.Bar} />
</li>);
}
}
</ul>
)}
/>
</Form>
)}
/>
</div>
);
}
}
You can do this via setting enableReinitialize true. According to doc it will do this:
Default is false. Control whether Formik should reset the form if initialValues changes (using deep equality).
I created complete codesanbox where your incoming data is async and when you push the data its also async. check this:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const InviteFriends = () => {
const [initialValues, setInitialValues] = React.useState({
friends: []
});
useEffect(() => {
const initialValues = {
friends: [
{
name: "",
email: ""
}
]
};
const timer = setTimeout(() => {
setInitialValues(initialValues);
}, 1000);
return () => {
timer && clearTimeout(timer);
};
}, []);
return (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{({ values }) => (
<Form>
<FieldArray name="friends">
{({ insert, remove, push }) => (
<div>
{console.log("Values", values, initialValues)}
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>
Email
</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={async () => {
await new Promise((r) =>
setTimeout(() => {
push({ name: "", email: "" });
r();
}, 500)
);
}}
>
Add Friend
</button>
</div>
)}
</FieldArray>
<button type="submit">Invite</button>
</Form>
)}
</Formik>
</div>
);
};
ReactDOM.render(<InviteFriends />, document.getElementById("root"));
Here is the demo: https://codesandbox.io/s/react-formik-async-l2cc5?file=/index.js
When I press delete button, it only deletes the last element every time, regardless to index.
How can I do this proper way? Without changing <input defaultValue={name} /> to <input value={name} /> in Child component?, I've done it with <input defaultValue={name} />, but how could I do this with value property to an input?
export const App = () => {
const [names, setNames] = React.useState([
"First",
"Second",
"third",
"fourth",
]);
const onChange = (params) => {
doing(params); // etc.
};
function onDelete(index: number) {
const nameArr = [...names];
nameArr.splice(index, 1);
setNames(nameArr);
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={index}
name={name}
index={index}
onChange={onChange}
onDelete={handleDelete}
/>
))}
</div>
);
};
const ChildComponent = React.memo(({ name, index, onChange, onDelete }) => {
return (
<div>
<input
defaultValue={name}
onChange={(event) => onChange(index, event.target.value)}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
});
You have a typo in your code (onDelete and handleDelete) but this is not the main issue. Your problem is using index as your key. Since you are removing some elements attached to the state, React can't decide how to sort the array again, because index here is not trustable.
You should use some unique values for your keys, if there is not, you should create them somehow. In the below example I try to mix index with the value:
key={`${index}${name}`}
Also, do not use methods like splice since they mutate the state. splice change elements in place, so mutation. I used filter but you can use any other method which doesn't mutate the state.
function App() {
const [names, setNames] = React.useState([
"First",
"Second",
"third",
"fourth"
]);
function onDelete(index) {
setNames((prev) => prev.filter((_, i) => i !== index));
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={`${index}${name}`}
name={name}
index={index}
onDelete={onDelete}
/>
))}
</div>
);
}
const ChildComponent = React.memo(({ name, index, onChange, onDelete }) => {
return (
<div>
<input
defaultValue={name}
onChange={(event) => onChange(index, event.target.value)}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
});
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
I am setting up a form, and in the form there's a material ui select field ranges from 1 to 50. How am I going to dynamically display/render multiple textfields "Firstname and Lastname" with Hooks in each text fields every time the user select a number?
Ex.
If user chose "3" from the option in the select box, there's expected to be 3 textfields "firstname and lastname" to be rendered.
This is what I have tried so far, I know how to create hooks, with specific textfield but I don't know if I will depend on the user selection.
const primary_guest_firstname = useForm('');
const primary_guest_lastname = useForm('');
const primary_guest_adult = useForm(1);
function useForm(init){
const [ value, setValue ] = useState(init);
function handleOnChange(e){
setValue(e.target.value);
}
return {
value,
onChange: handleOnChange
}
}
/** sample of select text field with options from 1 - 50... **/
<TextField
select
variant="outlined"
required
margin="dense"
fullWidth
value={primary_guest_adult.value}
onChange={primary_guest_adult.onChange}
id="guest"
label="Adult ( > 12 years)"
name="guest"
>
<MenuItem value="1">1</MenuItem>
<MenuItem value="2">2</MenuItem>
<MenuItem value="3">3</MenuItem>
...
<MenuItem value="50">50</MenuItem>
</TextField>
/** I should render dynamic textfield here...
if the user selected "3", there should be 3 textfields
generated here with hook.. **/
<TextField
variant="outlined"
required
margin="dense"
fullWidth
id="firstName"
label="First Name"
name="firstName"
value={primary_guest_firstname.value}
onChange={primary_guest_firstname.onChange}
/>
<TextField
variant="outlined"
required
margin="dense"
fullWidth
id="lastName"
label="Last Name"
name="lastName"
value={primary_guest_lastname.value}
onChange={primary_guest_lastname.onChange}
/>
I appreciate your help :) Thanks!
You can do this by creating an array of required values in the state array.
Instead of single state, you would have an array of states, which needs to be set and read accordingly.
Demo:
const { render } = ReactDOM;
const { useState, useEffect } = React;
function useForm(init){
const [ value, setValue ] = useState(init);
function handleOnChange(e){
setValue(e.target.value);
}
return {
value,
onChange: handleOnChange
}
}
function useForms(reqNum) {
const initial = [...Array(reqNum)].map(()=>({ firstName: "", lastName: "" }));
const [values, setValues] = useState(initial);
function handleChange(event, name, changedIndex) {
const { target: { value }} = event;
setValues(values => values.map((val, index) => {
if(changedIndex === index) {
return {
...val,
[name]: value,
}
}
return val;
}));
}
function handleNumChange(changedNum) {
const changedValues = [...Array(+changedNum)].map(()=>({ firstName: "", lastName: "" }));
setValues(changedValues);
}
return {
values,
handleChange,
handleNumChange,
}
}
const NameFields = ({ values }) => {
return (
<div>
{values.map((value, index) => (
<div key={index}>
<input placeholder={`First Name ${index+1}`} value={value.firstName} onChange={(e) => handleChange(e, "firstName", index)} />
<input placeholder={`Last Name ${index+1}`} value={value.lastName} onChange={(e) => handleChange(e, "lastName", index)} />
</div>
))}
</div>
);
}
const App = () => {
const options = [1,2,3,4,5];
const {value: numberOfAdults, onChange: setNumberOfAdults} = useForm(1);
const { values, handleChange, handleNumChange } = useForms(numberOfAdults);
useEffect(() => {
handleNumChange(numberOfAdults);
}, [numberOfAdults]);
return (
<main>
<select value={numberOfAdults} onChange={setNumberOfAdults}>
{options.map((option)=>(
<option value={option} key={option}>{option}</option>
))}
</select>
<NameFields values={values} />
</main>
);
}
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" />