yourmystar tech blog
著者: nishino 公開日:

Blitz.js による無限リストの作り方(おまけ編)

この記事はユアマイスター アドベントカレンダー 2022の 21 日目の記事です。

前回のおさらい

前回(アドベントカレンダー 2022 の 14 日目)、および前々回(アドベントカレンダー 2022 の 7 日目)の記事で、Blitz.js による無限リストを作りました。

今回は、この無限リストに関するおまけ的な実装を行います。

この記事でできること

前々回の記事で、Blitz.js による無限リストを作りました。その時は、一覧画面しか使用しなかったので特に問題はなかったのですが、一覧以外の画面は日付型をサポートしておらず、登録、編集ができませんでした。

今回は、登録、編集画面の日付型について対応を行い、画面から登録できるようにします。

Blitz.js とは何か?については前々回の記事に記載していますので、そちらを参照してください。

現在の状態確認

一覧画面で、「Create Schedule」をクリックすると「Create New Schedule」画面に遷移します。

01_list.png

新規登録画面

02_new1.png

現在の状態は、コード生成された時のテンプレートデフォルトのままです。

アプリケーションの修正

Schedule モデルは、コード生成テンプレートデフォルトの name項目ではなく、 titledate 項目に変更したので、以下のファイルを変更します。

  • src/schedules/components/ScheduleForm.tsx
diff --git a/src/schedules/components/ScheduleForm.tsx b/src/schedules/components/ScheduleForm.tsx
index a7af6c9..b24a5c7 100644
--- a/src/schedules/components/ScheduleForm.tsx
+++ b/src/schedules/components/ScheduleForm.tsx
@@ -6,7 +6,8 @@ export { FORM_ERROR } from "src/core/components/Form"
 export function ScheduleForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
   return (
     <Form<S> {...props}>
-      <LabeledTextField name="name" label="Name" placeholder="Name" />
+      <LabeledTextField name="title" label="Title" placeholder="Title" />
+      <LabeledTextField name="date" label="Date" placeholder="Date" />
     </Form>
   )
 }

この状態で、データを登録すると、以下のエラーメッセージが表示されます。

💡 ZodError: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "name" ], "message": "Required" } ]

03_new2.png

Blitz では、データのバリデーションに Zod というライブラリをしています。

TypeScript-first schema validation with static type inference

Zod は、typescript による型機能を使用してバリデーションを行うツールです。

エラーメッセージの内容は、「 name という項目( path )は、必須( Required )であり、 string を期待していたが、 undefined が返ってきている、という内容です。

先ほどは入力フォーム(クライアントサイド)の項目を変更しましたが、今回はサーバーサイドでの型チェックに引っ掛かっています。

なので、サーバーサイドの作成と更新時に実行される以下のファイルの Zod の型定義部分を name項目から、 titledate 項目に変更します。

  • src/schedules/mutations/createSchedule.ts
  • src/schedules/mutations/updateSchedule.ts
diff --git a/src/schedules/mutations/createSchedule.ts b/src/schedules/mutations/createSchedule.ts
index f487549..bc2ae39 100644
--- a/src/schedules/mutations/createSchedule.ts
+++ b/src/schedules/mutations/createSchedule.ts
@@ -3,7 +3,8 @@ import db from "db"
 import { z } from "zod"

 const CreateSchedule = z.object({
-  name: z.string(),
+  title: z.string(),
+  date: z.date(),
 })

 export default resolver.pipe(resolver.zod(CreateSchedule), resolver.authorize(), async (input) => {
diff --git a/src/schedules/mutations/updateSchedule.ts b/src/schedules/mutations/updateSchedule.ts
index 8600b68..aae3dd1 100644
--- a/src/schedules/mutations/updateSchedule.ts
+++ b/src/schedules/mutations/updateSchedule.ts
@@ -4,7 +4,8 @@ import { z } from "zod"

 const UpdateSchedule = z.object({
   id: z.number(),
-  name: z.string(),
+  title: z.string(),
+  date: z.date(),
 })

 export default resolver.pipe(

再度、新規データを追加して確認してみます。

04_new3.png

エラーメッセージが以下に変わりました。

💡 ZodError: [ { "code": "invalid_type", "expected": "date", "received": "string", "path": [ "date" ], "message": "Expected date, received string" } ]

エラーメッセージの内容は、「 date という項目( path )は、日付型であり、 date を期待していたが、 string が返ってきている」という内容です。

インストール時にフォームのライブラリとして、React Final Form を選択していますが、Blitz のコード生成によって作成された フォームのコンポーネントが 日付型に対応していないので、これを以下のように修正して日付型に対応します。

  • src/core/components/LabeledTextField.tsx
diff --git a/src/core/components/LabeledTextField.tsx b/src/core/components/LabeledTextField.tsx
index 7dc98b8..fc645aa 100644
--- a/src/core/components/LabeledTextField.tsx
+++ b/src/core/components/LabeledTextField.tsx
@@ -1,5 +1,6 @@
 import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef } from "react"
 import { useField, UseFieldConfig } from "react-final-form"
+import { format, parse } from "date-fns"

 export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
   /** Field name. */
@@ -7,7 +8,7 @@ export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElem
   /** Field label. */
   label: string
   /** Field type. Doesn't include radio buttons and checkboxes */
-  type?: "text" | "password" | "email" | "number"
+  type?: "text" | "password" | "email" | "number" | "date"
   outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
   labelProps?: ComponentPropsWithoutRef<"label">
   fieldProps?: UseFieldConfig<string>
@@ -15,15 +16,32 @@ export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElem

 export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
   ({ name, label, outerProps, fieldProps, labelProps, ...props }, ref) => {
+    const parseField = (value: any) => {
+      if (!value) return value
+      switch (props.type) {
+        case "number":
+          return Number as any
+        case "date":
+          return parse(value, "yyyy-MM-dd", new Date())
+        default:
+          return value === "" ? null : value
+      }
+    }
+    const formatField = (value: any) => {
+      if (!value) return value
+      switch (props.type) {
+        case "date":
+          return format(value, "yyyy-MM-dd")
+        default:
+          return value
+      }
+    }
     const {
       input,
       meta: { touched, error, submitError, submitting },
     } = useField(name, {
-      parse:
-        props.type === "number"
-          ? (Number as any)
-          : // Converting `""` to `null` ensures empty values will be set to null in the DB
-            (v) => (v === "" ? null : v),
+      parse: parseField,
+      format: formatField,
       ...fieldProps,
     })

修正にあたっては、React Final Form の以下のドキュメントを参考にしました。

useField のリファレンス

Final Form Docs - useField()

FieldProps のリファレンス

Final Form Docs - FieldProps

また、 type="date" をサポートしたので、以下のファイルも修正します。

  • src/schedules/components/ScheduleForm.tsx
diff --git a/src/schedules/components/ScheduleForm.tsx b/src/schedules/components/ScheduleForm.tsx
index b24a5c7..ebf1295 100644
--- a/src/schedules/components/ScheduleForm.tsx
+++ b/src/schedules/components/ScheduleForm.tsx
@@ -7,7 +7,7 @@ export function ScheduleForm<S extends z.ZodType<any, any>>(props: FormProps<S>)
   return (
     <Form<S> {...props}>
       <LabeledTextField name="title" label="Title" placeholder="Title" />
-      <LabeledTextField name="date" label="Date" placeholder="Date" />
+      <LabeledTextField name="date" label="Date" placeholder="Date" type="date" />
     </Form>
   )
 }

修正内容の確認

新規登録画面

新規登録画面にて、データを入力し、Create Schedule ボタンをクリックします。

05_new-done1.png

エラーが発生せず新規登録できました。

06_new-done2.png

編集画面

続いて「Edit」リンクをクリックします。

07_edit-done1.png

「Update Schedule」ボタンをクリックして更新します。

08_edit-done2.png

こちらも問題なく更新できました。

まとめ

Blitz のコード生成で作成されるフォーム(React Final Form)が日付型に対応していなかったのを対応できました。

ツイートするはてなブックマークに追加シェアする