'How can I edit this TextField?
I have an Material UI text field which is populated by a nested JSON object pulled from an API.
Data can be displayed in either a TextField, a Date Picker, or a Select box. This is decided by FieldType
.
Data is displaying in the TextField's, Date Pickers & Select boxes just fine, but it cannot be changed. Trying to change the text in any of the inputs results in this error msg: Uncaught TypeError: prev.fields is not iterable
.
Below is my method for displaying data in their respective inputs.
{details["groups"]?.map((group) => {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{group?.GroupName}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box>
{group["fields"]?.map((row, index) => {
if (
row?.FieldType === "Text" ||
row?.FieldType === "Decimal" ||
row?.FieldType === "Number"
) {
return (
<TextField
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update
});
}}
margin="normal"
label={row["FieldName"]}
/>
);
}
if (row?.FieldType === "Date") {
return (
<TextField
type="date"
value={row?.Value || null}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update
});
}}
label={row["FieldName"]}
InputLabelProps={{
shrink: true,
}}
/>
);
} else {
return (
<TextField
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update
});
}}
select
label={row?.FieldName}
>
{row?.Choices.map((choice) => (
<MenuItem key={choice} value= {choice}>
{choice}
</MenuItem>
))}
</TextField>
);
}
})}
</Box>
</AccordionDetails>
</Accordion>
);
})}
An Example of my JSON:
{
"groups": [
{
"GroupName": "Details",
"GroupOrder": 1,
"fields": [
"FieldId": 2,
"FieldName": "Day",
"FieldType": "Select",
"Value": "Option1",
"Choices": [
"Option1",
"Option2"
]
]
},
{
"GroupName": "Attributes",
"GroupOrder": 2,
"fields": [
{
"FieldId": 2,
"FieldName": "Night",
"FieldType": "Text",
"Value": "Night time",
"Choices": [
null
]
},
{
"FieldId": 3,
"FieldName": "Todays Date",
"FieldType": "Date",
"Value": "2020-08-12",
"Choices": [
null
]
}
],
}
]
}
API call:
const params = useParams();
const [details, setDetails] = useState('');
const fetchDetails = async () => {
setDetails(
await fetch(`/fiscalyears/FY2023/intakes/${params.id}/details`).then(
(response) => response.json()
)
);
};
useEffect(() => {
fetchDetails();
}, []);
Is it possible to use one method to create textFields for multiple nested JSON objects rather than hard coding them?
Solution 1:[1]
Eventually refactored your code. Plenty of code can be written better.
Uncaught TypeError: prev.fields is not iterable.
is because you haven't pass the groupIndex in the handleChange
.
The working solution is put onto the Github repo.
Have a glance on the main code:
// noinspection JSIgnoredPromiseFromCall
import './App.css';
import {Accordion, AccordionDetails, AccordionSummary, Box, MenuItem, TextField, Typography} from "@mui/material";
import {useEffect, useState} from "react";
import YourJSON from "./YourJSON";
const App = () => {
const [details, setDetails] = useState({});
const fetchDetails = async () => {
// imitate an api call
setDetails(await YourJSON());
};
const generalFieldTypes = (fieldType) => {
return ["Text", "Decimal", "Number"].includes(fieldType);
}
const dateFieldTypes = (fieldType) => {
return fieldType === 'Date';
}
const handleChange = (e, fieldIndex, groupIndex) => {
console.log(JSON.stringify(details));
let update = details;
update['groups'][groupIndex]['fields'][fieldIndex]['Value'] = e.target.value;
console.log(JSON.stringify(update));
setDetails(update);
}
const DynamicField = (field, fieldIndex, groupIndex) => {
const {FieldId, FieldName, FieldType, Value, Choices} = field;
if (generalFieldTypes(FieldType)) {
return <TextField
defaultValue={Value}
onChange={
(e) => handleChange(e, fieldIndex, groupIndex)
}
label={FieldName}
InputLabelProps={{shrink: true}}
/>;
} else if (dateFieldTypes(FieldType)) {
return <TextField
type="date"
defaultValue={Value}
onChange={
(e) => handleChange(e, fieldIndex, groupIndex)
}
label={FieldName}
InputLabelProps={{shrink: true}}
/>;
} else {
return <TextField
defaultValue={Value}
onChange={
(e) => handleChange(e, fieldIndex, groupIndex)
}
select
label={FieldName}
>
{
Choices.map((choice) => (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
))
}
</TextField>;
}
}
useEffect(() => {
fetchDetails();
}, []);
return (
<div className="App">
{details && details["groups"]?.map((group, groupIndex) => {
const {GroupName, GroupOrder, fields} = group;
return (
<Accordion>
{/*I won't have your expandIcon, thereby remove it*/}
<AccordionSummary>
<Typography>{GroupName}</Typography>
</AccordionSummary>
<AccordionDetails>
<Box>
{
fields.map((field, fieldIndex) => {
return DynamicField(field, fieldIndex, groupIndex);
})
}
</Box>
</AccordionDetails>
</Accordion>
);
}
)}
</div>
);
}
export default App;
Solution 2:[2]
You just need to make minor changes in your onChange function and pass the 'index' parameter on map function
{questions["Days"]?.map((row, index) => (
<TextField
fullWidth
multiline
className="text-field"
value={row?.Response || ""}
onChange={(e) => {
setQuestions((prev) => {
const days = [...prev.Days];
days[index] = {
...days[index],
Response: e.target.value
};
return { ...prev, Days: days };
});
}}
variant="outlined"
margin="normal"
label={row["Question"]}
/>
))}
Console the state on update with your function and then console it with the function I provided, you will see the difference.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |