ご意見送信フォームを作成する③ [SPA by React Router]
概要
この記事について
かんたんな概要と結論
以前作成したReact製フォームをReact RouterでSPA化した。
モーダルポップアップとして表示していた結果画面を別ページに移し、モーダル特有の問題からも解放された。
モーダルポップアップとして表示していた結果画面を別ページに移し、モーダル特有の問題からも解放された。
先日作成したReact製意見送信フォームでは、
フォームのSubmit後にモーダルポップアップとして送信完了および結果の取得ボタンを表示していたが、
実はモーダル画面特有の問題を抱えていた。
問題:
- (必須)モーダル以外の部分を操作できたら困るので
モーダル表示時、モーダル部分の要素のみフォーカスできるように固定しなければならない(Focus Trap) - (オプション)WAI-ARIA対応としてアクセシビリティ向上のために、モーダルのコンポーネントにaria属性の設定などが必要
上記のそれぞれの問題についてはこちらでは対応していないので、
モーダルを使うのをやめてSPA化して別のページにしてしまうことを考え、作成することとした。
SPA化のためにはReact Routerを使用している。
レスポンシブ対応について
PCでの閲覧のみ対応。
スマホなどで閲覧時にはレイアウトが崩れる。
要件の変更
SPAするにあたり、次のように要件を変更し、機能追加するようにした。
- 構成の変更
- Home画面を追加する
- ヘッダー、フッターを追加する
- 送信の結果表示画面をモーダルから単独のページに変更
- 動作の変更
- フォームが入力済みの場合、他のページに遷移する際に警告を表示するようにする
- 結果表示ページはフォーム上でSubmitした場合にのみ表示(URL指定で遷移しようとした場合はHomeにリダイレクトする)
設計
クラス設計
About
フォームのロジックや、受理内容.txtファイルの作成ロジックなどは変更なし。
Appコンポーネントと各childコンポーネントとのデータのI/Oを一部変更
(こちらのほうが前回よりもI/O周りがすっきりしている印象)。
クラス図
実装
作成環境
1node v14.16.1
2npm 6.14.12
3create-react-app
依存パッケージ
一部抜粋。
1{
2 "dependencies":{
3 "react": "^17.0.2",
4 "react-dom": "^17.0.2",
5 "react-scripts": "4.0.3",
6 "typescript": "^4.4.3",
7 "moment": "^2.29.1",
8 "node-sass": "^6.0.1",
9 "react-hook-form": "^7.16.1",
10 "react-router-dom": "^5.3.0"
11 }
12}
13
ディレクトリ構成
一部抜粋。
1my-app
2│ package-lock.json
3│ package.json
4│ tsconfig.json
5│
6├─node_modules
7│
8├─public
9│ index.html
10│
11└─src
12 │ index.tsx
13 │ react-app-env.d.ts
14 │ setupTests.ts
15 │
16 ├─style
17 │ style.scss
18 │ _active.scss
19 │ _footer.scss
20 │ _form.scss
21 │ _header.scss
22 │ _main-section.scss
23 │ _mixin.scss
24 │ _result.scss
25 │ _variables.scss
26 │
27 └─ts
28 │ App.test.tsx
29 │ App.tsx
30 │
31 ├─error-pages
32 │ nomatch.tsx
33 │
34 ├─form
35 │ opinion-form.test.tsx
36 │ opinion-form.tsx
37 │
38 ├─header
39 │ footer.tsx
40 │ header.tsx
41 │
42 ├─home
43 │ home.tsx
44 │
45 └─result-page
46 submit-indicator.test.tsx
47 submit-indicator.tsx
48
コード
ソースコードの全体はこちらから
App Component
Routerライブラリを用いて、
指定されたURLごとに表示するページを切り分けている。
/ | Homeページ |
/opinion-form | フォームページ |
/result | 送信結果表示ページ |
画面遷移のナビゲートの役割はHeader Component
が持っている。
"/result"のURLにはフォームSubmit時のみ移動できるようにしているため、
条件が揃わなければリダイレクトさせる。
1function App() {
2 const [isSubmitted, setIsSubmitted] = useState(false);
3 const [formValues, setFormValues] = useState<accepttedContentData>(
4 {} as accepttedContentData
5 );
6 const handleSubmit = useCallback(
7 (data: IFormInput) => {
8 setFormValues({
9 name: data.fname,
10 gender: data.fgender,
11 age: data.fage,
12 address: data.faddress,
13 message: data.fmessage,
14 });
15
16 setIsSubmitted(true);
17 },
18 [setIsSubmitted]
19 );
20
21 return (
22 <>
23 <Header />
24 <main>
25 <div className="content">
26 <Switch>
27 <Route exact path="/">
28 <Home />
29 </Route>
30 <Route path="/opinion-form">
31 <div className="normal-form-container">
32 <OpinionForm onSubmit={handleSubmit}></OpinionForm>
33 </div>
34 </Route>
35 <Route path="/result">
36 {isSubmitted ? (
37 <SubmitIndicator
38 formValues={formValues}
39 setIsSubmitted={setIsSubmitted}
40 />
41 ) : (
42 <Redirect to="/" />
43 )}
44 </Route>
45 {/* for dev */}
46 {process.env.NODE_ENV === "development" && (
47 <DevResultDummyData />
48 )}
49 {/* 404 error page */}
50 <Route path="*">
51 <NoMatch />
52 </Route>
53 </Switch>
54 </div>
55 </main>
56 <Footer />
57 </>
58 );
59}
Header Component
Link Component
により画面遷移をナビゲートする。
今回はHomeページとフォームページのみ。
1export function Header() {
2 return (
3 <header className="nav-header">
4 <h1>Header</h1>
5 <nav>
6 <ul>
7 <li>
8 <Link to="/">Home</Link>
9 </li>
10 <li>
11 <Link to="/opinion-form">ご意見フォーム</Link>
12 </li>
13 {(process.env.NODE_ENV === "development") && (
14 <li>
15 <DevResultLink />
16 </li>
17 )}
18 </ul>
19 </nav>
20 </header>
21 )
22}
OpinionForm Component
他画面遷移時にPrompt Component
が働き、
入力の中断を問い合わせる。
1
2export function OpinionForm({ onSubmit }: OpinionFormArgs) {
3 ......
4 <Prompt
5 when={isDirty && (!onSubmitting)}
6 message={location =>
7 `フォームに入力中です。${location.pathname}に移動しますか?`
8 }
9 />
10 ......
11}
SubmitIndicator Component
table要素内で送信結果をユーザに表示できるように変更した。
1export function SubmitIndicator({
2 formValues,
3 setIsSubmitted,
4}: ISubmitIndicatorArgs) {
5
6 ......
7
8 return (
9 <>
10 <div id="result-container">
11 <div id="result-box">
12 <h2 id="result-message">ご意見を受け付けました。</h2>
13 <table>
14 <tbody>
15 <tr>
16 <td>氏名</td>
17 <td>{formValues.name}</td>
18 </tr>
19 <tr>
20 <td>性別</td>
21 <td>{parseGender(formValues.gender)}</td>
22 </tr>
23 <tr>
24 <td>年齢</td>
25 <td>{formValues.age} 歳</td>
26 </tr>
27 <tr>
28 <td>住所</td>
29 <td>{formValues.address}</td>
30 </tr>
31 <tr>
32 <td>ご意見内容</td>
33 <td>{formValues.message}</td>
34 </tr>
35 </tbody>
36 </table>
37 <p id="result-date">受理日時:{submiteedDate}</p>
38 <button
39 id="result-download"
40 onClick={onClickDownload}
41 >
42 送信内容のダウンロード
43 </button>
44 </div>
45 </div>
46 </>
47 );
48}
デモ(Vercel)
関連記事
- ご意見送信フォームを作成する② [React]
- ご意見送信フォームを作成する① [プレーンTypeScript]
- [Javascript] IE11でRadioボタングループのvalueを取得できない事象&対策