Search code examples
reactjsreact-hooksnext.jsmaterial-uivercel

Get All TextField values from loop in Next.js when I press Submit button


first of all look at these below screenshots: enter image description here

enter image description here

There are two tasks which I want to achieve:

  1. There are two questions shown on the page using the array map method, by default I'm showing only one question, and when I press the next part button the second question will appear with the same question and a TextField (multiline). Now I've implemented a word counter in TextField but when I type something in 1st question the counter works properly. But when I go to the next question the here counter shows the previous question's word counter value, I want them to separately work for both questions.
  2. When I click on the next part and again when I click on the previous part then the values from TextField are removed automatically. I want the values there if I navigate to the previous and next part questions. Also, I want to get both TextField values for a form submission when I press the Submit Test button.

Below are my codes for this page. I'm using Next.js and MUI

import { Grid, Typography, Box, NoSsr, TextField } from '@mui/material';
import PersonIcon from '@mui/icons-material/Person';
import Timer from '../../../components/timer';
import Button from '@mui/material/Button';
import { useState } from 'react';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { Scrollbars } from 'react-custom-scrollbars';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import axios from '../../../lib/axios';
import { decode } from 'html-entities';
import { blueGrey } from '@mui/material/colors';

export default function Writing({ questions }) {
    const [show, setShow] = useState(false);
    const [show1, setShow1] = useState(true);

    const [showQuestionCounter, setShowQuestionCounter] = useState(0);
    const [wordsCount, setWordsCount] = useState(0);

    return (
        <>
            <Box sx={{ flexGrow: 1 }}>
                <AppBar position="fixed" style={{ background: blueGrey[900] }}>
                    <Toolbar>
                        <Grid container spacing={2} alignItems="center">
                            <Grid item xs={4} display="flex" alignItems="center">
                                <PersonIcon
                                    sx={{ background: '#f2f2f2', borderRadius: '50px' }}
                                />
                                <Typography variant="h6" color="#f2f2f2" ml={1}>
                                    xxxxx xxxxx-1234
                                </Typography>
                            </Grid>
                            <Grid item xs={4} container justifyContent="center">
                                <Timer timeValue={2400} />
                            </Grid>
                            <Grid item xs={4} container justifyContent={'right'}>
                                <Button
                                    variant="contained"
                                    style={{ background: 'white', color: 'black' }}
                                    size="small">
                                    Settings
                                </Button>
                                <Button
                                    variant="contained"
                                    style={{
                                        background: 'white',
                                        color: 'black',
                                        margin: '0px 10px',
                                    }}
                                    size="small">
                                    Hide
                                </Button>
                                <Button
                                    variant="contained"
                                    style={{ background: 'white', color: 'black' }}
                                    size="small">
                                    Help
                                </Button>
                            </Grid>
                        </Grid>
                    </Toolbar>
                </AppBar>
            </Box>

            <Box
                sx={{
                    background: blueGrey[50],
                    height: '100%',
                    width: '100%',
                    position: 'absolute',
                }}
                pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}>
                {questions.map((question, index) =>
                    index === showQuestionCounter ? (
                        <Box
                            key={question.id}
                            px={3}
                            sx={{ background: '#f2f2f2', pb: 4 }}
                            position={{
                                xs: 'sticky',
                                sm: 'sticky',
                                lg: 'initial',
                                md: 'initial',
                                xl: 'initial',
                            }}>
                            <Box
                                style={{ background: '#f7fcff', borderRadius: '4px' }}
                                py={1}
                                px={2}>
                                <Box>
                                    <Typography variant="h6" component="h6" ml={1}>
                                        Part {question.id}
                                    </Typography>
                                    <Typography variant="subtitle2" component="div" ml={1} mt={1}>
                                        <NoSsr>
                                            <div
                                                dangerouslySetInnerHTML={{
                                                    __html: decode(question.questions[0].question, {
                                                        level: 'html5',
                                                    }),
                                                }}></div>
                                        </NoSsr>
                                    </Typography>
                                </Box>
                            </Box>

                            <Box
                                style={{
                                    background: '#f7fcff',
                                    borderRadius: '4px',
                                    marginBottom: '75px',
                                }}
                                pt={1}
                                px={3}
                                mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}>
                                <Grid container spacing={2}>
                                    <Grid item xs={12} sm={12} lg={6} md={6} xl={6}>
                                        <Box
                                            py={{ lg: 1, md: 1, xl: 1 }}
                                            style={{ height: '50vh' }}>
                                            <Scrollbars universal>
                                                <Typography
                                                    variant="body1"
                                                    component="div"
                                                    style={{ textAlign: 'justify' }}
                                                    mr={2}>
                                                    <NoSsr>
                                                        <div
                                                            dangerouslySetInnerHTML={{
                                                                __html: decode(question.question_text, {
                                                                    level: 'html5',
                                                                }),
                                                            }}></div>
                                                    </NoSsr>
                                                </Typography>
                                            </Scrollbars>
                                        </Box>
                                    </Grid>
                                    <Grid
                                        item
                                        xs={12}
                                        sm={12}
                                        lg={6}
                                        md={6}
                                        xl={6}
                                        mt={{ md: 4, lg: 4, xl: 4 }}>
                                        <TextField
                                            id={`${question.id}`}
                                            label="Type your answer here"
                                            multiline
                                            name={`answer_${question.id}`}
                                            rows={12}
                                            variant="outlined"
                                            fullWidth
                                            helperText={`Words Count: ${wordsCount}`}
                                            onChange={(e) => {
                                                setWordsCount(
                                                    e.target.value.trim().split(/\s+/).length
                                                );
                                            }}
                                        />
                                    </Grid>
                                </Grid>
                            </Box>
                        </Box>
                    ) : null
                )}

                <Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}>
                    <Grid
                        container
                        style={{ background: blueGrey[300], display: 'flex' }}
                        py={2}
                        px={3}>
                        <Grid
                            item
                            xs={3}
                            sm={3}
                            lg={6}
                            md={6}
                            xl={6}
                            container
                            justifyContent={'start'}>
                            <Button
                                variant="contained"
                                style={{ background: 'white', color: 'black' }}
                                size="small">
                                Save Draft
                            </Button>
                        </Grid>
                        <Grid
                            item
                            xs={9}
                            sm={9}
                            lg={6}
                            md={6}
                            xl={6}
                            container
                            justifyContent={'end'}>
                            <Button
                                variant="contained"
                                size="small"
                                style={{
                                    background: 'white',
                                    color: 'black',
                                    visibility: show1 ? 'visible' : 'hidden',
                                }}
                                endIcon={<ArrowForwardIosIcon />}
                                onClick={() => {
                                    setShow((prev) => !prev);
                                    setShowQuestionCounter(showQuestionCounter + 1);
                                    setShow1((s) => !s);
                                }}>
                                Next Part
                            </Button>
                            {show && (
                                <>
                                    <Box>
                                        <Button
                                            variant="contained"
                                            style={{
                                                background: 'white',
                                                color: 'black',
                                                margin: '0 10px',
                                                visibility: show ? 'visible' : 'hidden',
                                            }}
                                            startIcon={<ArrowBackIosIcon />}
                                            size="small"
                                            onClick={() => {
                                                setShow1((s) => !s);
                                                setShowQuestionCounter(showQuestionCounter - 1);
                                                setShow((prev) => !prev);
                                            }}>
                                            previous Part
                                        </Button>
                                        <Button variant="contained" color="success">
                                            Submit Test
                                        </Button>
                                    </Box>
                                </>
                            )}
                        </Grid>
                    </Grid>
                </Box>
            </Box>
        </>
    );
}

export async function getServerSideProps(context) {
    const { id } = context.query;
    const token = context.req.cookies.token;

    if (!token) {
        context.res.writeHead(302, {
            Location: '/',
        });
        context.res.end();
    }

    const res = await axios.get(`test/${id}/questions`, {
        headers: {
            Authorization: `Bearer ${token}`,
        },
    });
    if (res.data.success) {
        return {
            props: {
                questions: res.data.data.questions,
            },
        };
    }
}


Solution

    1. The wordsCount state is shared between both questions, which means that when you go to the next question the state remains unchanged and shows the wordsCount from the first question. To solve it, each question needs to have it's own state which you can do by creating a Question component and mapping over it:
    export default function Question({ question }) {
      const [wordsCount, setWordsCount] = useState(0)
    
      return (
        <Box
          // ...
        >
          {/* ... */}
          <TextField
            // ...
            helperText={`${wordsCount} words`}
            onChange={(e) => {
              setWordsCount(e.target.value.trim().split(/\s+/).length)
            }}
          />
          {/* ... */}
        </Box>
      )
    }
    

    Then map over it:

    {questions.map((question, index) =>
       index === showQuestionCounter ? (
         <Question key={question.id} question={question} />
       ) : null
    )}
    
    1. Currently, the value of TextField gets reset you the component is unmounted (i.e. when you go to the next question). You need to make the TextField component a controlled component, meaning that you store the value of the field in useState. And if you need to submit the value of TextField later, then you probably need to store the values in the parent component:
    export default function Writing({ questions }) {
      // ...
      const [answers, setAnswers] = useState([])
    
      function handleChange(id, answer) {
        // copy the current answers
        let newAnswers = [...answers]
        // find the index of the id of the answer in the current answers
        const index = newAnswers.findIndex((item) => item.id === id)
        // if the answer does exist, replace the previous answer with the new one, else add the new answer
        if (index) {
          newAnswers[index] = { id, answer }
          setAnswers(newAnswers)
        } else {
          setAnswers([...answers, { id, answer }])
        }
      }
    
      return (
        <>
            {/* ... */}
            {questions.map((question, index) =>
              index === showQuestionCounter ? (
                <Question
                  key={question.id}
                  question={question}
                  value={answers.find((item) => item.id === question.id)?.answer || ''}
                  handleChange={handleChange}
                />
              ) : null
            )}
      </>
    }
    

    In the Question component, add the handler.

    export default function Question({ question, value, handleInputChange }) {
      const [wordsCount, setWordsCount] = useState(0)
    
      return (
          <Box>
            {/* ... */}
            <TextField
              helperText={`${wordsCount} words`}
              value={value}
              onChange={(e) => {
                handleInputChange(question.id, e.target.value)
                setWordsCount(e.target.value.trim().split(/\s+/).length)
              }}
            />
            {/* ... */}
          </Box>
      )
    }
    

    In the parent component (Writing) you should be able to use the values for form submission.