Reputation: 305
I'm using Material UI 5.7.0. I've created a survey form that users can fill out and click a button to download a json file with their answers. I'd like to manage my questions in json object called questionsObject
. It seemed to make sense to use this same object (or one created dynamically from it) to load the initial state of the form as well as updating it.
The below code is working BUT I've statically defined my default values in the defaultValues object. I'll need to update that every time I add a new question to the questions object. I really want it to read the default value from the value field in the questionsObject.
import React, {useEffect, useState} from "react";
import Grid from "@mui/material/Grid";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import FormLabel from "@mui/material/FormLabel";
import RadioGroup from "@mui/material/RadioGroup";
import Radio from "@mui/material/Radio";
import Button from "@mui/material/Button";
import Select from '@mui/material/Select';
import { saveAs } from 'file-saver';
import {InputLabel, MenuItem} from "@mui/material";
const questionsObject = {
q1: {
questionText: "Animals",
type: "BOTH",
displayType: "select",
menuItems: [
'',
"Dogs",
"Cats",
"Elephants"
],
value: ''
},
q2: {
questionText: "Favorite Pizza Topping",
type: "BOTH",
displayType: "select",
menuItems: [
'',
"Pepperoni",
"Mushrooms"
],
value: ''
},
q3: {
questionText: "Question text",
type: "BOTH",
displayType: "radioGroup",
value: "true"
},
q4: {
questionText: "Question text",
type: "BOTH",
displayType: "radioGroup",
value: "no answer"
},
q5: {
questionText: "Question text",
type: "BE",
displayType: "radioGroup",
value: "no answer"
}
}
const defaultValues = {
q1: '',
q2: '',
q3: "no answer",
q4: "no answer",
q5: "no answer"
}
const MyForm = () => {
const [formValues, setFormValues] = useState(defaultValues);
// useEffect(() => {
// const outputObj = {}
// Object.keys(questionsObject).map((question) =>
// outputObj[question.id] = question.value
// );
// setFormValues(outputObj)
// }, []);
const handleInputChange = (e) => {
const { name, value} = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = (event) => {
event.preventDefault();
const outputObj = {}
Object.keys(questionsObject).map((question) =>
outputObj[question.id] = {
questionText: question.questionText,
type: question.type,
answer: formValues[question.id]
}
);
console.log(outputObj);
const outputObjJSON = JSON.stringify(outputObj, null, 2);
const blob = new Blob([outputObjJSON], {type: "application/json"});
saveAs(blob, "answerfile.json");
};
const getQuestionElement = (questionId) => {
const question = questionsObject[questionId];
switch(question.displayType) {
case "radioGroup":
return (
<Grid item key={questionId}>
<FormControl>
<FormLabel>{question.questionText}</FormLabel>
<RadioGroup
questiontype = {question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
>
<FormControlLabel
value="no answer"
control={<Radio size="small" />}
label="No Answer"
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="False"
/>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="True"
/>
<FormControlLabel
value="NA"
control={<Radio size="small" />}
label="Not Applicable"
/>
</RadioGroup>
</FormControl>
</Grid>
);
case "select":
return (
<Grid item key={questionId}>
<FormControl sx={{ m: 2, minWidth: 200 }}>
<InputLabel>{question.questionText}</InputLabel>
<Select
name={questionId}
value={formValues[questionId]}
label={question.questionText}
onChange={handleInputChange}
>
{question.menuItems.map((item, index) =>
<MenuItem key={index} value={item}>{item}</MenuItem>
)}
</Select>
</FormControl>
</Grid>
);
default:
}
}
return formValues ? (
<form onSubmit={handleSubmit}>
<Grid
container
spacing={0}
direction="column"
alignItems="left"
justifyContent="center">
{Object.keys(questionsObject).map((questionId =>
getQuestionElement(questionId)
))}
<Button variant="contained" color="primary" type="submit">
Download
</Button>
</Grid>
</form>
): null ;
};
export default MyForm;
I tried starting with no default state like this:
const [formValues, setFormValues] = useState();
and uncommenting useEffect() but I get this error:
MUI: You have provided an out-of-range value `undefined` for the select component.
Consider providing a value that matches one of the available options or ''.
The available values are ``, `Dogs`, `Cats`, `Elephants`.
on top of that my Radio Buttons have no default value set.
This lead me to attempting to use the defaultValue fields on both the RadioGroup and the Select component.
Attempting to set initial values in the defaultValue property like this:
defaultValue=""
or
defaultValue={question.value}
Give me this error:
MUI: A component is changing the uncontrolled value state of Select to be controlled.
Elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled Select element for the lifetime of the component.
The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.
How can I remove the defaultValues object and use the value field inside of questionsObject for the initial state instead?
Upvotes: 0
Views: 1184
Reputation: 6036
The problem is in the logic inside your useEffect
:
useEffect(() => {
const outputObj = {}
Object.keys(questionsObject).map(
(question) => outputObj[question.id] = question.value
);
console.log(outputObj);
setFormValues(outputObj)
}, []);
/// OUTPUT of outputObj:
{undefined: undefined}
You should change it to:
useEffect(() => {
const outputObj = {};
Object.keys(questionsObject).map(
(question) => (outputObj[question] = questionsObject[question].value)
);
console.log(outputObj);
setFormValues(outputObj);
}, []);
/// OUTPUT of outputObj:
{q1: "", q2: "", q3: "true", q4: "no answer", q5: "no answer"}
Also, avoid the switch/case
- mainly for not doing anything with default
value. In this case prefer the conditional renderering with &&
operator (docs):
const getQuestionElement = (questionId) => {
const question = questionsObject[questionId];
return (
<React.Fragment key={questionId}>
{question.displayType === "radioGroup" && (
<Grid item>
<FormControl>
<FormLabel>{question.questionText}</FormLabel>
<RadioGroup
questiontype={question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
>
<FormControlLabel
value="no answer"
control={<Radio size="small" />}
label="No Answer"
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="False"
/>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="True"
/>
<FormControlLabel
value="NA"
control={<Radio size="small" />}
label="Not Applicable"
/>
</RadioGroup>
</FormControl>
</Grid>
)}
{question.displayType === "select" && (
<Grid item>
<FormControl sx={{ m: 2, minWidth: 200 }}>
<InputLabel>{question.questionText}</InputLabel>
<Select
name={questionId}
value={formValues[questionId]}
label={question.questionText}
onChange={handleInputChange}
>
{question.menuItems.map((item, index) => (
<MenuItem key={index} value={item}>
{item}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}
</React.Fragment>
);
};
Upvotes: 1