'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