LoginSignup
1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-02

【要件】

1. 基本情報入力画面

(1) 性別と生年月日を入力できる
(2) 『進む』ボタン

2. アンケート画面

(1) 最初に表示されている設問は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のコンポーネントでラップします。

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年以内に病気やケガで手術を受けたことまたは継続して7日以上の入院をしたことはありますか?",
];

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を利用したルーティング設定の基礎
【React TypeScript】react -router-domの導入!Material UIのボタンを使って画面遷移をしよう!
Reactでアプリを作成しました【7】【マルチステップの入力フォーム】
react-routerとreact-router-domの違い
react-routerでページ遷移にちょっとしたアニメーションを付ける

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0