CloudFlareにデプロイするためのRemixプロジェクトで別画面で一覧の検索条件を付与する

## 概要

下記でクエリパラメーターの値に伴って一覧に表示する項目を絞り込む処理を作成した。
今回は別画面で一覧に表示する条件を設定し、一覧画面に遷移させてみようと思う。

https://qiita.com/miriwo/items/c438c9c377ae28fac2e7

## 詳細

トップページに検索条件を選択するウインドウ的なものを用意し、検索条件を入力後「検索する」ボタンを押下するとクエリパラメーターが入った状態の一覧画面に遷移させたい。

## 前提

下記記事の内容が完了していること

https://qiita.com/miriwo/items/c438c9c377ae28fac2e7

## 方法

### トップ画面`/`から`/tasks`に遷移するボタンの設置

まずは下記の方法でトップ画面からクエリパラメーター無しの`/tasks`に遷移するボタンを設置する。

1. `app/routes/_index.tsx`を下記のように修正し、ボタンを設置

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate } from "@remix-run/react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
    
      const handleSearchClick = () => {
        navigate("/tasks"); // 一覧画面に遷移
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. `npm run dev`を行い、トップ画面にアクセスし「登録する」をクリックし、クエリパラメーターがついていない一覧が表示されることを確認

    ![CleanShot 2024-06-16 at 12.14.58.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/2db4cd57-c0a1-d5a0-a7f5-e75d8b17a824.gif)

1. `app/routes/_index.tsx`を下記のように修正し、一旦クエリパラメーターをハードコーディング

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate } from "@remix-run/react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
    
      const handleSearchClick = () => {
        navigate("/tasks?isDeleted=true"); // 遷移先ページ指定
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. トップ画面にアクセスし「登録する」をクリックし、`isDeleted=true`クエリパラメーターがついている一覧が表示されることを確認

    ![CleanShot 2024-06-16 at 12.19.38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/ebdce546-cf99-0c4d-e5a5-3493f7abd903.gif)

### 絞り込み内容を入力する場所の追加

一気に複数項目の絞り込みメニューを作ると複雑になりそうなのでまずはisDeletedだけを絞り込むフィールドを追加してみる。

1. `app/routes/_index.tsx`を下記のように修正し、削除済みタスクだけを絞り込むフィールドを追加

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate } from "@remix-run/react";
    import { useState } from "react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
      const [isDeleted, setIsDeleted] = useState(false);
    
      const handleSearchClick = () => {
        const queryParams = new URLSearchParams({ isDeleted: isDeleted.toString() });
        navigate(`/tasks?${queryParams.toString()}`); // クエリパラメーターを含めて遷移
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isDeleted}
                  onChange={(e) => setIsDeleted(e.target.checked)}
                />
                削除済みを表示
              </label>
            </div>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. トップ画面にアクセスし、「削除済みを表示」のチェックボックスをチェックつけたり、外したりして「登録する」をクリックし、`isDeleted=true`クエリパラメーターのtrueやfalseが設定されて、一覧が表示されることを確認

    ![CleanShot 2024-06-16 at 12.44.12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/fd20e1c2-bda9-34ff-939e-24acb4934d17.gif)

1. 今は「チェックボックスがONでもOFFでも、必ずクエリパラメーターがtrueかfalseで付与」されるが`app/routes/_index.tsx`を下記のように修正することで「チェックボックスがONの場合だけ、クエリパラメーターのtrueを付与」というように実装が可能。要件にもよるがURLがシンプルになるので筆者はこちらを選択（表示内容は変化しないようにtasks-loader.tsを記載しているので変わらない）（URLがシンプルになる分、_index.tsxのコードが若干複雑になる）

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate } from "@remix-run/react";
    import { useState } from "react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
      const [isDeleted, setIsDeleted] = useState(false);
    
      const handleSearchClick = () => {
        const queryParams = new URLSearchParams();
        if (isDeleted) {
          queryParams.append("isDeleted", isDeleted.toString());
        }
        navigate(`/tasks?${queryParams.toString()}`); // クエリパラメーターを含めて遷移
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isDeleted}
                  onChange={(e) => setIsDeleted(e.target.checked)}
                />
                削除済みを表示
              </label>
            </div>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. 下記のような挙動になる。「チェックボックスつけたときだけクエリパラメーターが付与」されているところに注目

    ![CleanShot 2024-06-16 at 12.48.44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/8b14fd55-b7f5-5c2e-180f-77bf7d881eaa.gif)

### 「完了可否」の検索条件の入力フィールドを追加

1. `app/routes/_index.tsx`を下記のように修正し、「完了可否」の検索条件入力フィールドとクエリパラメーターの作成部分を追加

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate } from "@remix-run/react";
    import { useState } from "react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
      const [isDeleted, setIsDeleted] = useState(false);
      const [isComplete, setIsComplete] = useState(false);
    
      const handleSearchClick = () => {
        const queryParams = new URLSearchParams();
        if (isDeleted) {
          queryParams.append("isDeleted", isDeleted.toString());
        }
        if (isComplete) {
          queryParams.append("isComplete", isComplete.toString());
        }
        navigate(`/tasks?${queryParams.toString()}`); // クエリパラメーターを含めて遷移
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isDeleted}
                  onChange={(e) => setIsDeleted(e.target.checked)}
                />
                削除済みを表示
              </label>
            </div>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isComplete}
                  onChange={(e) => setIsComplete(e.target.checked)}
                />
                完了済みを表示
              </label>
            </div>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. 動作確認は最後にまとめて実施予定

### 「タスクカテゴリ」の検索条件の入力フィールドを追加

「タスクカテゴリ」は複数項目存在するのでプルダウンで入力フィールドを用意する


1. `app/routes/_index.tsx`を下記のように修正し、「タスクカテゴリ」の検索条件入力フィールドとクエリパラメーターの作成部分を追加

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate } from "@remix-run/react";
    import { useState } from "react";
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
      const [isDeleted, setIsDeleted] = useState(false);
      const [isComplete, setIsComplete] = useState(false);
      const [taskCategoryMasterId, setTaskCategoryMasterId] = useState(""); // 初期値を空文字列として定義
    
      const handleSearchClick = () => {
        const queryParams = new URLSearchParams();
        if (isDeleted) {
          queryParams.append("isDeleted", isDeleted.toString());
        }
        if (isComplete) {
          queryParams.append("isComplete", isComplete.toString());
        }
        if (taskCategoryMasterId !== "") {
          queryParams.append("taskCategoryMasterId", taskCategoryMasterId.toString());
        }
        navigate(`/tasks?${queryParams.toString()}`); // クエリパラメーターを含めて遷移
      };
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isDeleted}
                  onChange={(e) => setIsDeleted(e.target.checked)}
                />
                削除済みを表示
              </label>
            </div>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isComplete}
                  onChange={(e) => setIsComplete(e.target.checked)}
                />
                完了済みを表示
              </label>
            </div>
            <div>
              <label>
                タスクカテゴリ:
                <select
                  onChange={(e) => setTaskCategoryMasterId(e.target.value)}
                  defaultValue=""
                >
                  <option value="" disabled>選択してください</option>
                  <option value="1">仕事</option>
                  <option value="2">勉強</option>
                  <option value="3">その他</option>
                </select>
              </label>
            </div>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. これだと「タスクカテゴリが仕事 かつ 勉強」という設定ができないが、それは次の記事で実装予定

### ココまでの動作確認

1. トップを開き「完了済みを表示」にチェックを入れてクエリパラメーターが正常に入ること、一覧表示が正常なことを確認

    ![CleanShot 2024-06-16 at 13.16.01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/5c284602-7177-683b-4072-7d3803f677b5.gif)

1. トップを開き「タスクカテゴリ」を選びクエリパラメーターが正常に入ること、一覧表示が正常なことを確認

    ![CleanShot 2024-06-16 at 13.17.28.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/06370ec7-2b13-ea81-5b34-911fec6619aa.gif)

1. 最後に複合的な条件で検索を行い、クエリパラメーターが問題なく表示されれば一旦ココまでは完了

### タスクカテゴリプルダウンをテーブルから取得した情報で表示する

現状、タスクカテゴリのプルダウンの要素はハードコーディングされている。これだとtasks_category_mastersテーブルに要素を追加してもこのプルダウンの要素は増えない。
そのためプルダウンの要素をtasks_category_mastersテーブルから取得した要素を表示するように変更する。

1. `app/routes/task-category-masters-loader.ts`のファイルを作成し下記のように記載

    ```app/routes/task-category-masters-loader.ts
    import type { LoaderFunctionArgs} from "@remix-run/cloudflare";
    
    export const loader = async ({ context }: LoaderFunctionArgs) => {
      try {
        return await context.db.task_category_masters.findMany();
      } catch (error) {
        console.error("Failed to load users:", error);
        throw new Response("Internal Server Error", { status: 500 });
      }
    }
    ```

1. `app/routes/_index.tsx`を下記のように修正

    ```app/routes/_index.tsx
    import type { MetaFunction } from "@remix-run/cloudflare";
    import { useNavigate, useLoaderData } from "@remix-run/react";
    import { useState } from "react";
    import { loader } from "./task-category-masters-loader";
    export { loader };
    
    export const meta: MetaFunction = () => {
      return [
        { title: "Top" },
        {
          name: "description",
          content: "Top",
        },
      ];
    };
    
    export default function Index() {
      const navigate = useNavigate();
      const [isDeleted, setIsDeleted] = useState(false);
      const [isComplete, setIsComplete] = useState(false);
      const [taskCategoryMasterId, setTaskCategoryMasterId] = useState(""); // 初期値を空文字列として定義
    
      const handleSearchClick = () => {
        const queryParams = new URLSearchParams();
        if (isDeleted) {
          queryParams.append("isDeleted", isDeleted.toString());
        }
        if (isComplete) {
          queryParams.append("isComplete", isComplete.toString());
        }
        if (taskCategoryMasterId !== "") {
          queryParams.append("taskCategoryMasterId", taskCategoryMasterId.toString());
        }
        navigate(`/tasks?${queryParams.toString()}`); // クエリパラメーターを含めて遷移
      };
    
      interface TaskCategoryMaster {
      id: string;
      name: string;
    }
    
      const taskCategoryMasters = useLoaderData<TaskCategoryMaster[]>();
    
      return (
        <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
          <div>
            <h2>タスク管理</h2>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isDeleted}
                  onChange={(e) => setIsDeleted(e.target.checked)}
                />
                削除済みを表示
              </label>
            </div>
            <div>
              <label>
                <input
                  type="checkbox"
                  checked={isComplete}
                  onChange={(e) => setIsComplete(e.target.checked)}
                />
                完了済みを表示
              </label>
            </div>
            <div>
              <label>
                タスクカテゴリ:
                <select
                  onChange={(e) => setTaskCategoryMasterId(e.target.value)}
                  defaultValue=""
                >
                  <option value="" disabled>選択してください</option>
                  {taskCategoryMasters.map((taskCategoryMaster) => (
                    <option key={taskCategoryMaster.id} value={taskCategoryMaster.id}>
                      {taskCategoryMaster.name}
                    </option>
                  ))}
                </select>
              </label>
            </div>
            <button onClick={handleSearchClick}>検索する</button>
          </div>
        </div>
      );
    }
    ```

1. 下記のようにプルダウンのnameの値がtask_category_mastersのnameの値で設定されていることを確認

    ![CleanShot 2024-06-17 at 22.04.41.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/c2b2e050-018b-58f8-60a1-e3f7b5982c22.gif)