【要件】
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を使用するように書き換えする。
"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
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 を編集
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コンポーネントから単純なスイッチを返し、アプリをその新しいコンポーネントでラップする。
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"]
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 を編集
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 を編集
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 を編集
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 を編集
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でページ遷移にちょっとしたアニメーションを付ける