ご意見送信フォームを作成する③ [SPA by React Router]

概要

この記事について

かんたんな概要と結論

以前作成したReact製フォームをReact RouterでSPA化した。
モーダルポップアップとして表示していた結果画面を別ページに移し、モーダル特有の問題からも解放された。

先日作成したReact製意見送信フォームでは、
フォームのSubmit後にモーダルポップアップとして送信完了および結果の取得ボタンを表示していたが、
実はモーダル画面特有の問題を抱えていた。

問題:

  • (必須)モーダル以外の部分を操作できたら困るので
    モーダル表示時、モーダル部分の要素のみフォーカスできるように固定しなければならない(Focus Trap)
  • (オプション)WAI-ARIA対応としてアクセシビリティ向上のために、モーダルのコンポーネントにaria属性の設定などが必要

上記のそれぞれの問題についてはこちらでは対応していないので、
モーダルを使うのをやめてSPA化して別のページにしてしまうことを考え、作成することとした。

SPA化のためにはReact Routerを使用している。

ソースコードはこちらにあります(GitHub)。

レスポンシブ対応について

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
1011└─src
12    │  index.tsx
13    │  react-app-env.d.ts
14    │  setupTests.ts
1516    ├─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
2627    └─ts
28        │  App.test.tsx
29        │  App.tsx
3031        ├─error-pages
32        │      nomatch.tsx
3334        ├─form
35        │      opinion-form.test.tsx
36        │      opinion-form.tsx
3738        ├─header
39        │      footer.tsx
40        │      header.tsx
4142        ├─home
43        │      home.tsx
4445        └─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)

Vercelにデプロイしました。

関連記事

comments powered by Disqus