Reactでアプリを作成しました【10】【マルチステップの入力フォーム ②】

##【要件】

####1. 基本情報入力画面

(1) 性別と生年月日を入力できる
(2) 『進む』ボタン
####2. アンケート画面

(1) 最初に表示されている設問は１つのみ
(2) 設問に答えると次の設問が表示される
(3) 『戻る』ボタンと『進む』ボタン
####3. 相談内容入力画面

(1) Textareaに自由に入力できる
(2) 『戻る』ボタンと『進む』ボタン
(3) 『進む』ボタンは動作しなくても良い
####4. 確認画面

(1) 入力した内容が表示される
(2) 『戻る』ボタンと『送信』ボタン
(3) 『送信』ボタンは動作しなくても良い

##Reactアプリの作成する
```
$ npx create-react-app <アプリ名>
```
##必要なパッケージのインストール
① Material-UI を利用するため、事前にライブラリをインストールする。

```
// with npm
$ npm i @mui/material @emotion/react @emotion/styled

// with yarn
$ yarn add @mui/material @emotion/react @emotion/styled
```
② react-router-dom をインストールする。
ナビゲーションにはReactRouterが必要で、遷移動作にはFramerMotionが必要。

```
$ npm i react-router-dom framer-motion
```
③ react-app-rewiredをインストールする。

```
$ yarn add react-app-rewired
```
④ packge.jsonのnpm scriptをreact-app-rewiredを使用するように書き換えする。

```packge.json
"scripts": {
        "start": "react-app-rewired start",
        "build": "react-app-rewired build",
        "test": "react-app-rewired test",
        "eject": "react-scripts eject"
    },
```
⑤ config-overrides.jsファイルを作成し、webpackConfig.module.rulesをオーバーライドする。

```
$ touch config-overrides
```
```config-overrides.js
module.exports = function override(webpackConfig) {
    webpackConfig.module.rules.push({
        test: /.mjs$/,
        include: /node_modules/,
        type: 'javascript/auto'
    });
 
    return webpackConfig;
};
```


##ソースコード

```
src
├── components
│   ├── Basic.js
│   ├── Confirm.js
│   ├── Optional.js
│   └── Questionnaire.js     
├── App.js
└── paramUtil.js 
```

##index.js を編集

```index.js 
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import reportWebVitals from "./reportWebVitals";
import App from "./App";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

reportWebVitals();
```

##App.js を編集

①BrowserRouterをアプリに追加する。
②アニメーションを追加しましょう。まず、FramerMotion<Switch>の<AnimatePresence>コンポーネントでラップします。

```
import { Routes, Route, useLocation } from "react-router-dom";
import { AnimatePresence } from "framer-motion";
```
③Appコンポーネントから単純なスイッチを返し、アプリをその新しいコンポーネントでラップする。

```App.js
import React from "react";
import { Routes, Route, useLocation } from "react-router-dom";
import { AnimatePresence } from "framer-motion";
import Basic from "./components/Basic";
import Questionnaire from "./components/Questionnaire";
import Optional from "./components/Optional";
import Confirm from "./components/Confirm";
import { AppBar, Toolbar } from "@mui/material";

const App = () => {
  const location = useLocation();
  return (
    <>
      <AppBar position="static" style={{ backgroundColor: "primary" }}>
        <Toolbar>React課題 ④</Toolbar>{" "}
      </AppBar>
      <AnimatePresence exitBeforeEnter initial={false}>
        <Routes location={location} key={location.pathname}>
          <Route path="/" element={<Basic />}></Route>
          <Route path="/" element={<Questionnaire />}></Route>
          <Route path="/" element={<Optional />}></Route>
          <Route path="/" element={<Confirm />}></Route>
        </Routes>
      </AnimatePresence>
    </>
  );
};
export default App;
```

##paramUtil.js  を編集


① 任意のオブジェクトからクエリパラメータ用の文字列配列を作成する

```
 eg. { a: "aaa", b: null, c : 1} ===> ["a=aaa", "c=1"]
```
   
```paramUtil.js

export const createParamArray = (obj) => {
  if (!obj) return [];
  return Object.keys(obj).reduce((acc, cur) => {
    if (obj[cur]) {
      acc.push(`${cur}=${obj[cur]}`);
    }
    return acc;
  }, []);
};
```

##components / Basic.js  を編集

```components/Basic.js

import React from "react";
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import {
  Button,
  FormControl,
  FormControlLabel,
  FormLabel,
  InputLabel,
  Radio,
  RadioGroup,
  Select,
} from "@mui/material";
import { createParamArray } from "../paramUtil";

export const createBasicParameter = (basicProfile) => {
  return {
    gender: basicProfile?.gender ?? undefined,
    year: basicProfile?.year ?? undefined,
    month: basicProfile?.month ?? undefined,
    day: basicProfile?.day ?? undefined,
  };
};

const Basic = ({
  isConfirm,
  data = {
    gender: null,
    year: null,
    month: null,
    day: null,
  },
}) => {
  const [basicProfile, setBasicProfile] = React.useState(data);

  const paramArray = React.useMemo(() => {
    return createParamArray(createBasicParameter(basicProfile));
  }, [basicProfile]);

  return (
    <>
      {!isConfirm ? (
        <p style={{ textAlign: "center" }}>お客様の情報を入力して下さい</p>
      ) : null}

      <motion.div
        initial={{ scaleY: 0 }}
        animate={{ scaleY: 1 }}
        exit={{ scaleY: 0 }}
        transition={{ duration: 0.5 }}
      >
        <div>
          <div style={{ textAlign: "center" }}>
            <FormControl component="fieldset">
              <FormLabel component="gender">- 性別 -</FormLabel>
              {isConfirm ? (
                <span>{basicProfile?.gender === "male" ? "男性" : "女性"}</span>
              ) : (
                <RadioGroup
                  row
                  aria-label="gender"
                  name="row-radio-buttons-group"
                  value={basicProfile.gender}
                  onChange={(evt) =>
                    setBasicProfile((state) => {
                      return { ...state, gender: evt.target.value };
                    })
                  }
                >
                  <FormControlLabel
                    value="male"
                    control={<Radio />}
                    label="男性"
                  />
                  <FormControlLabel
                    value="female"
                    control={<Radio />}
                    label="女性"
                  />
                </RadioGroup>
              )}
            </FormControl>
          </div>
          <div style={{ textAlign: "center" }}>
            <FormLabel component="legend">- 生年月日 -</FormLabel>
            <FormControl sx={{ m: 1, minWidth: 120 }}>
              <InputLabel htmlFor="grouped-native-select">year</InputLabel>
              {isConfirm ? (
                <span>{basicProfile.year}</span>
              ) : (
                <Select
                  native
                  defaultValue=""
                  id="grouped-native-select"
                  label="Grouping"
                  value={basicProfile.year}
                  onChange={(evt) =>
                    setBasicProfile((state) => {
                      return { ...state, year: evt.target.value };
                    })
                  }
                >
                  <option aria-label="None" value="" />
                  <optgroup label="year">
                    {Array.from(Array(2020), (_, num) => (
                      <option key={num} value={num + 1990}>
                        {num + 1990}
                      </option>
                    ))}
                  </optgroup>
                </Select>
              )}
            </FormControl>
            <FormControl sx={{ m: 1, minWidth: 120 }}>
              <InputLabel htmlFor="grouped-native-select">month</InputLabel>
              {isConfirm ? (
                <span>{basicProfile.month}</span>
              ) : (
                <Select
                  native
                  defaultValue=""
                  id="grouped-native-select"
                  label="Grouping"
                  value={basicProfile.month}
                  onChange={(evt) =>
                    setBasicProfile((state) => {
                      return { ...state, month: evt.target.value };
                    })
                  }
                >
                  <option aria-label="None" value="" />
                  <optgroup label="month">
                    {Array.from(Array(12), (_, num) => (
                      <option key={num} value={num + 1}>
                        {num + 1}
                      </option>
                    ))}
                  </optgroup>
                </Select>
              )}
            </FormControl>
            <FormControl sx={{ m: 1, minWidth: 120 }}>
              <InputLabel htmlFor="grouped-native-select">day</InputLabel>
              {isConfirm ? (
                <span>{basicProfile.day}</span>
              ) : (
                <Select
                  native
                  defaultValue=""
                  id="grouped-native-select"
                  label="Grouping"
                  value={basicProfile.day}
                  onChange={(evt) =>
                    setBasicProfile((state) => {
                      return { ...state, day: evt.target.value };
                    })
                  }
                >
                  <option aria-label="None" value="" />
                  <optgroup label="day">
                    {Array.from(Array(31), (_, num) => (
                      <option key={num} value={num + 1}>
                        {num + 1}
                      </option>
                    ))}
                  </optgroup>
                </Select>
              )}
            </FormControl>
          </div>
          {!isConfirm ? (
            <div style={{ textAlign: "center" }}>
              <Link
                to={{
                  pathname: `/Questionnaire?${paramArray.join("&")}`,
                }}
              >
                <Button variant="contained" size="medium">
                  次へ
                </Button>
              </Link>
            </div>
          ) : null}
        </div>
      </motion.div>
    </>
  );
};
export default Basic;
```

##components / Confirm.js を編集

```components/Confirm.js

import React from "react";
import { motion } from "framer-motion";
import { Link, useLocation } from "react-router-dom";
import Basic from "./Basic";
import Questionnaire from "./Questionnaire";
import Optional from "./Optional";
import { Button } from "@mui/material";
import { createBasicParameter } from "./Basic";

export const UserInputData = React.createContext();

function Confirm() {
  const { search } = useLocation();
  const query = React.useMemo(() => new URLSearchParams(search), [search]);
  const gender = query.get("gender");
  const year = query.get("year");
  const month = query.get("month");
  const day = query.get("day");
  const answers = query.get("answers");
  const consultation = query.get("consultation");

  const basicProfile = React.useMemo(() => {
    return createBasicParameter({
      gender,
      year,
      month,
      day,
    });
  }, [gender, year, month, day]);

  return (
    <motion.div
      initial={{ scaleY: 0 }}
      animate={{ scaleY: 1 }}
      exit={{ scaleY: 0 }}
      transition={{ duration: 0.5 }}
    >
      <div>
        <p style={{ textAlign: "center" }}>以下の内容をご確認下さい</p>
        <div style={{ textAlign: "center" }}>
          <Basic isConfirm data={basicProfile} />
          <Questionnaire isConfirm data={answers} />
          <Optional isConfirm data={consultation} />
        </div>
        <div style={{ textAlign: "center" }}>
          <Link to="/Optional">
            <Button variant="outlined" size="medium">
              戻る
            </Button>
          </Link>
          <Link to="/">
            <Button variant="contained" size="medium">
              送信
            </Button>
          </Link>
        </div>
      </div>
    </motion.div>
  );
}
export default Confirm;
```

##components / Optional.js を編集

```components/Optional.js

import React from "react";
import { motion } from "framer-motion";
import { Link, useLocation } from "react-router-dom";
import { Button, Grid, TextField, Tooltip } from "@mui/material";
import { createBasicParameter } from "./Basic";
import { createParamArray } from "../paramUtil";

const Optional = ({ isConfirm, data }) => {
  const [optionalRequest, setOptionalRequest] = React.useState({
    request: null,
    consultation: data ?? "",
  });
  const { search } = useLocation();
  const query = React.useMemo(() => new URLSearchParams(search), [search]);
  const gender = query.get("gender");
  const year = query.get("year");
  const month = query.get("month");
  const day = query.get("day");
  const answers = query.get("answers");

  const basicProfile = React.useMemo(() => {
    return createBasicParameter({
      gender,
      year,
      month,
      day,
    });
  }, [gender, year, month, day]);

  const paramArray = React.useMemo(() => {
    return createParamArray({
      ...basicProfile,
      answers,
      consultation: optionalRequest?.consultation ?? undefined,
    });
  }, [basicProfile, answers, optionalRequest]);

  return (
    <>
      {!isConfirm ? <p style={{ textAlign: "center" }}>ご相談下さい</p> : null}

      <motion.div
        initial={{ scaleY: 0 }}
        animate={{ scaleY: 1 }}
        exit={{ scaleY: 0 }}
        transition={{ duration: 0.5 }}
      >
        <div style={{ textAlign: "center" }}>
          <div>
            <Grid container>
              <Grid sm={2} />
              <Grid lg={8} sm={8} spacing={10}>
                {isConfirm ? (
                  <span>{optionalRequest?.consultation}</span>
                ) : (
                  <Tooltip
                    title="ご相談内容を記入することができます"
                    placement="top-start"
                    arrow
                    value={optionalRequest.consultation}
                    onChange={(evt) =>
                      setOptionalRequest((state) => {
                        return { ...state, consultation: evt.target.value };
                      })
                    }
                  >
                    <TextField
                      label="ご相談内容"
                      fullWidth
                      margin="normal"
                      rows={4}
                      multiline
                      variant="outlined"
                      placeholder="その他ご要望等あれば、ご記入ください"
                    />
                  </Tooltip>
                )}
              </Grid>
            </Grid>
          </div>
          {!isConfirm ? (
            <div style={{ textAlign: "center" }}>
              <Link to="/Questionnaire">
                <Button variant="outlined" size="medium">
                  戻る
                </Button>
              </Link>
              <Link
                to={{
                  pathname: `/Confirm?${paramArray.join("&")}`,
                }}
              >
                <Button variant="contained" size="medium">
                  次へ
                </Button>
              </Link>
            </div>
          ) : null}
        </div>
      </motion.div>
    </>
  );
};
export default Optional;
```


##components / Questionnaire.js を編集


```components/Questionnaire.js
import React from "react";
import { motion } from "framer-motion";
import { Link, useLocation } from "react-router-dom";
import {
  Button,
  FormControl,
  FormControlLabel,
  FormLabel,
  Radio,
  RadioGroup,
  Typography,
} from "@mui/material";
import { createBasicParameter } from "./Basic";
import { createParamArray } from "../paramUtil";

export const QUESTIONS = [
  "現在、生命保険に加入されていますか？",
  "現在、入院中ですか。また、3ヶ月以内に医師の診察・検査の結果、入院・手術をすすめられたことがありますか？",
  "過去、5年以内に病気やケガで手術を受けたことまたは継続して７日以上の入院をしたことはありますか？",
];

const Questionnaire = ({ isConfirm, data }) => {
  const handleAnswer = (answeredIndex, answer) => {
    setAnswers(answers.map((e, i) => (i === answeredIndex ? answer : e)));
  };
  const { search } = useLocation();
  const query = React.useMemo(() => new URLSearchParams(search), [search]);

  const gender = query.get("gender");
  const year = query.get("year");
  const month = query.get("month");
  const day = query.get("day");

  const basicProfile = React.useMemo(() => {
    return createBasicParameter({
      gender,
      year,
      month,
      day,
    });
  }, [gender, year, month, day]);

  const [answers, setAnswers] = React.useState(
    data ? data.split(",") : Array(QUESTIONS.length).fill(null)
  );

  const paramArray = React.useMemo(() => {
    return createParamArray({
      ...basicProfile,
      answers: answers.join(","),
    });
  }, [basicProfile, answers]);

  return (
    <>
      {!isConfirm ? (
        <p style={{ textAlign: "center" }}>以下の質問にお答え下さい</p>
      ) : null}

      <motion.div
        initial={{ scaleY: 0 }}
        animate={{ scaleY: 1 }}
        exit={{ scaleY: 0 }}
        transition={{ duration: 0.5 }}
      >
        <div style={{ textAlign: "center" }}>
          <FormControl component="fieldset">
            {answers
              .filter((_, i) => i === 0 || answers[i - 1])
              .map((answer, i) => (
                <React.Fragment key={i}>
                  <FormLabel component="legend">{QUESTIONS[i]}</FormLabel>
                  {isConfirm ? (
                    <Typography>
                      {answer === "yes" ? "はい" : "いいえ"}
                    </Typography>
                  ) : (
                    <RadioGroup
                      row
                      aria-label="gender"
                      name="row-radio-buttons-group"
                      onChange={(_evt, value) => {
                        handleAnswer(i, value);
                      }}
                    >
                      <FormControlLabel
                        value="yes"
                        control={<Radio />}
                        label="はい"
                      />
                      <FormControlLabel
                        value="no"
                        control={<Radio />}
                        label="いいえ"
                      />
                    </RadioGroup>
                  )}
                </React.Fragment>
              ))}
          </FormControl>
        </div>
        {!isConfirm ? (
          <div style={{ textAlign: "center" }}>
            <Link to="/">
              <Button variant="outlined" size="medium">
                戻る
              </Button>
            </Link>
            <Link
              to={{
                pathname: `/Optional?${paramArray.join("&")}`,
              }}
            >
              <Button variant="contained" size="medium">
                次へ
              </Button>
            </Link>
          </div>
        ) : null}
      </motion.div>
    </>
  );
};

export default Questionnaire;
```

##参考サイト
[入門者でもわかるReact Routerを利用したルーティング設定の基礎]
(https://reffect.co.jp/react/react-router)
[【React TypeScript】react -router-domの導入！Material UIのボタンを使って画面遷移をしよう！]
(https://teech-lab.com/react-typescript-react-router-dom-material-ui/1817/)
[Reactでアプリを作成しました【7】【マルチステップの入力フォーム】]
(https://qiita.com/kanfutrooper/items/6b663f94f03611b343e9)
[react-routerとreact-router-domの違い]
(https://qiita.com/koja1234/items/486f7396ed9c2568b235)
[react-routerでページ遷移にちょっとしたアニメーションを付ける]
(http://webdesign-dackel.com/2015/11/03/react-router-animate/)