Blitz.js による無限リストの作り方(おまけ編)
この記事はユアマイスター アドベントカレンダー 2022の 21 日目の記事です。
前回のおさらい
前回(アドベントカレンダー 2022 の 14 日目)、および前々回(アドベントカレンダー 2022 の 7 日目)の記事で、Blitz.js による無限リストを作りました。
今回は、この無限リストに関するおまけ的な実装を行います。
この記事でできること
前々回の記事で、Blitz.js による無限リストを作りました。その時は、一覧画面しか使用しなかったので特に問題はなかったのですが、一覧以外の画面は日付型をサポートしておらず、登録、編集ができませんでした。
今回は、登録、編集画面の日付型について対応を行い、画面から登録できるようにします。
Blitz.js とは何か?については前々回の記事に記載していますので、そちらを参照してください。
現在の状態確認
一覧画面で、「Create Schedule」をクリックすると「Create New Schedule」画面に遷移します。
新規登録画面
現在の状態は、コード生成された時のテンプレートデフォルトのままです。
アプリケーションの修正
Schedule モデルは、コード生成テンプレートデフォルトの name
項目ではなく、 title
と date
項目に変更したので、以下のファイルを変更します。
- 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" } ]
Blitz では、データのバリデーションに Zod というライブラリをしています。
TypeScript-first schema validation with static type inference
Zod は、typescript による型機能を使用してバリデーションを行うツールです。
エラーメッセージの内容は、「 name
という項目( path
)は、必須( Required
)であり、 string
を期待していたが、 undefined
が返ってきている、という内容です。
先ほどは入力フォーム(クライアントサイド)の項目を変更しましたが、今回はサーバーサイドでの型チェックに引っ掛かっています。
なので、サーバーサイドの作成と更新時に実行される以下のファイルの Zod の型定義部分を name
項目から、 title
と date
項目に変更します。
- 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(
再度、新規データを追加して確認してみます。
エラーメッセージが以下に変わりました。
💡 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 のリファレンス
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 ボタンをクリックします。
エラーが発生せず新規登録できました。
編集画面
続いて「Edit」リンクをクリックします。
「Update Schedule」ボタンをクリックして更新します。
こちらも問題なく更新できました。
まとめ
Blitz のコード生成で作成されるフォーム(React Final Form)が日付型に対応していなかったのを対応できました。