简体   繁体   中英

In React how do I set up a question form and run different answer-boxes that a user can click?

I have created a form like structure for users to chose one box and then click next like this (all pictures below). I use a map to display the boxes and when click next I am using state to load the next answerboxes. I have also a title that I have above the map as an ordinary p-tag. The way I have tested this on is that I use a counter (also state object) that I increment every time next is clicked. But the questions are stored in arrays (se code test) and I dont know how to implement this so that I can have a flow and a working way for both the title and the arrays of questions to show up when I click next. I was thinking of using props somehow but I cant seem to figure out the way to do it. As it is now I can loop through one array (with title) and display answers in qboxes but when I want to add the next automatically I cannot do it in any smooth way.

在此处输入图像描述

在此处输入图像描述 (qbox-answer clicked and next button clicked)

code:

//For looping through and have qboxes created. 
const testAnswers1 = ["answer 1", "answer 2", "answer 3", "answer 4", "answer 5", "answer 6"];
const testAnswers2 = ["answer 1", "answer 2", "answer 3", "answer 4", "answer 5", "answer 6"];
const testAnswers3 = ["answer 1", "answer 2", "answer 3", "answer 4", "answer 5", "answer 6"];

//Every array should have one title displayed until end
const questionTitles = ["title1", "title2", "title3", "title4"]



function ReportForm(props){

  const [title, setTitle] = useState(titles);
  const [qBoxes, setqBoxes] = useState(test);
  //const [isMouseOver, setMouseOver] = useState(false);

  //counter for title and qboxes
  let [counter, setCounter] = useState(0);
  //For storing answers
  let [answers, setAnswers] = useState([]);

  
//Code for handling hovering over an answerbox (css) and also collect the clicked answer

const handleClick = (event) => {

 
  if(event.currentTarget.classList.contains('active')) {
    event.currentTarget.classList.remove('active');

    answers.pop(event.currentTarget.value);
    

    
    console.log(event.currentTarget.value);
    
  } else {
    event.currentTarget.classList.add('active');

    setAnswers([...answers, event.target.value]);

    
  }
event.preventDefault();


}


//Next button code


const submitNext = (event) => {

  if (counter < titles.length -1){

    
      setqBoxes(qboxes[counter]);
      setCounter(counter = counter +1);
      setTitle(event.target.value);


  } else {
    //resets counter to last value so nothing happens when next is clicked
    setCounter(counter);
  }
 
}


//Code for map function

return (

    <div>
        <p>{questionTitles[counter]}</p>
        {qBoxes.map((e) => {

          return (
            <button
              
              onClick={handleClick}
              className="boxStyle"
              name="contentYes"
              value={e}
              onchange={handleChange}
              
            >{e}</button>
          )

        })};

        <button className="buttonNext" onClick={submitNext}>Next</button>
        <button className="buttonPrev" onClick={submitPrev}>Previous</button>


    </div>
  );

These things get much easier when using a pre-built form-handling library. Check out Formik or React-Hook-Form

For the wizard itself, consider wrapping each question (with answer array) in a separate component, so that it can't see or be bothered by the other questions.

Here is a recipe that is pretty declarative: Each question is its own component (you can map these from an array), and they are wrapped by a parent whose job it is to know where in the wizard the user is, what has been answered, and to only show the current question.

Wrapper

First of all, the Wrapper can use a state to remember where in the wizard it currently is

const increment = (x) => x + 1;
const decrement = (x) => x - 1;

const Wizard = ({ children }) => {
const maxCount = React.Children.count(children) - 1;
  const minCount = 0;

  const [current, setCurrent] = React.useState(minCount);

  const incrementCurrent = () => setCurrent(increment);
  const decrementCurrent = () => setCurrent(decrement);
  const isMaxCount = current === maxCount;
  const isMinCount = current === minCount;

...

Then, it can select the child component that corresponds to that state

...

return (
    <fieldset className="wizard">
      <legend>{name}</legend>

      {React.Children.toArray(children).at(current)}

      <button type="button" disabled={isMinCount} onClick={decrementCurrent}>
          prev
      </button>
      <button type="button" disabled={isMaxCount} onClick={incrementCurrent}>
          next
      </button>
    </fieldset>
)

You can try it out with just any children

<Wizard name="my wizard">
  <span>how</span>
  <span>YOU</span>
  <span>doin'</span>
</Wizard>

Add form-handling library

Everything gets easier if we use a library to help us handle user input in form-like situations. It probably need some kind of initialisation, so we do that in the parent as well, and make sure to pass whatever controls or registering functions that the children (questions components) are going to need. In this example, I went with React-Hook-Form

const form = useForm();

const onSubmit = () => {
   console.log(form.getValues())
}

return (
    ...
      <form onSubmit={form.handleSubmit(onSubmit)}>
        {React.cloneElement(React.Children.toArray(children).at(current), { form })}
    ...

Receiving the form handler in the question components

Each question can now use that form prop and interact with the form

Eg text input

const DescribeMarcellus = ({ form }) => (
    <article>
      <h2>Describe what Marsellus Wallace looks like</h2>
      <input type="text" {...form.register('description')} />
    </article>
  )

Or buttons like you have


const WhichCountry = ({ form }) => (
  <article>
    <h2>What country you from?</h2>
    <div className="flexCenter">
    {["What", "Tobleronistan", "Option 3"].map((ans) => (
      <div
        key={ans}
        className="option"
        style={{
          backgroundColor: form.getValues("country") === ans ? "blue" : "green"
        }}
        onClick={() => form.setValue("country", ans)}
      >
        {ans}
      </div>
      
    ))}
    </div>
  </article>
);

const SayWhatAgain = ({ form }) => (
  <article>
    <h2>
      <i>{form.getValues("country")}</i> ain't no country I ever heard of! They
      speak English in What?
    </h2>
    {["Yeah", "Nah"].map((ans) => (
      <div key={ans}>
        <input
          type="radio"
          id={ans}
          name="contact"
          value={ans}
          {...form.register("english")}
        />
        <label htmlFor={ans}>{ans}</label>
      </div>
    ))}
  </article>
);

Putting it all toghether:

import React from "react";
import { useForm } from "react-hook-form";
import "./styles.css";

const increment = (x) => x + 1;
const decrement = (x) => x - 1;

const Wizard = ({ name, children }) => {
  const maxCount = React.Children.count(children) - 1;
  const minCount = 0;
  const [current, setCurrent] = React.useState(minCount);
  const incrementCurrent = () => setCurrent(increment);
  const decrementCurrent = () => setCurrent(decrement);
  const isMaxCount = current === maxCount;
  const isMinCount = current === minCount;

  const form = useForm();

  const onSubmit = () => {
    console.log(form.getValues());
  };

  return (
    <fieldset className="wizard">
      <legend>{name}</legend>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        {React.cloneElement(React.Children.toArray(children).at(current), {
          form
        })}
        <button type="button" disabled={isMinCount} onClick={decrementCurrent}>
          prev
        </button>
        <button type="button" disabled={isMaxCount} onClick={incrementCurrent}>
          next
        </button>
        <button type="submit">submit</button>
      </form>
    </fieldset>
  );
};

const DescribeMarcellus = ({ form }) => (
  <article>
    <h2>Describe what Marsellus Wallace looks like</h2>
    <input type="text" {...form.register("description")} />
  </article>
);

const WhichCountry = ({ form }) => (
  <article>
    <h2>What country you from?</h2>
    <div className="flexCenter">
    {["What", "Tobleronistan", "Option 3"].map((ans) => (
      <div
        key={ans}
        className="option"
        style={{
          backgroundColor: form.getValues("country") === ans ? "blue" : "green"
        }}
        onClick={() => form.setValue("country", ans)}
      >
        {ans}
      </div>
      
    ))}
    </div>
  </article>
);

const SayWhatAgain = ({ form }) => (
  <article>
    <h2>
      <i>{form.getValues("country")}</i> ain't no country I ever heard of! They
      speak English in What?
    </h2>
    {["Yeah", "Nah"].map((ans) => (
      <div key={ans}>
        <input
          type="radio"
          id={ans}
          name="contact"
          value={ans}
          {...form.register("english")}
        />
        <label htmlFor={ans}>{ans}</label>
      </div>
    ))}
  </article>
);

export default function App() {
  return (
    <div className="App">
      <Wizard name="my wizard">
        <DescribeMarcellus />
        <WhichCountry />
        <SayWhatAgain />
      </Wizard>
    </div>
  );
}

codeSanbox Link: https://codesandbox.io/s/simple-wizard-sspt9u?file=/src/App.js

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM