技術検証用サンプルサイト


このサイトは?お問い合わせフォームの例CSVデータを型安全に扱う

実装ソースコード
"use client";

import {
  DependentSelectField,
  Form,
  type ItemsSelector,
  ResetButton,
  SelectField,
} from "@/ui/form";
import FormDebug from "@/ui/form/debug";
import { Button, Card, Container, Heading, HStack, Link } from "@yamada-ui/react";
import type { FC } from "react";
import * as R from "remeda";
import * as v from "valibot";
import carList from "./car-list.json";

type CarList = typeof carList;

type Manufacturer = keyof CarList;

type Model<T extends Manufacturer> = keyof CarList[T];

const manufacturers = R.keys(carList);

const models = <T extends Manufacturer>(manufacturer: T) => R.keys(carList[manufacturer]);

const grades = <T extends Manufacturer>(manufacturer: T, model: Model<T>) =>
  carList[manufacturer][model] as string[];

const hasModel = <T extends Manufacturer>(manufacturer: T, input: unknown) =>
  v.is(v.picklist(models(manufacturer)), input);

const hasGrade = <T extends Manufacturer>(manufacturer: T, model: Model<T>, input: unknown) =>
  v.is(v.picklist(grades(manufacturer, model)), input);

const schema = v.pipe(
  v.object({
    manufacturer: v.nonOptional(v.picklist(manufacturers)),
    model: v.nonOptional(v.string()),
    grade: v.nonOptional(v.string()),
  }),
  v.forward(
    v.check(
      (input) => hasModel(input.manufacturer, input.model),
      "選択肢に存在しないモデルが選ばれました。",
    ),
    ["model"],
  ),
  v.forward(
    v.check(
      (input) => hasGrade(input.manufacturer, input.model as never, input.grade),
      "選択肢に存在しないグレードが選ばれました。",
    ),
    ["grade"],
  ),
);

const modelItemsSelector: ItemsSelector<{ manufacturer: Manufacturer }> = ({ manufacturer }) =>
  R.keys(carList[manufacturer]);

const gradeItemsSelector: ItemsSelector<{ manufacturer: Manufacturer; model: string }> = ({
  manufacturer,
  model,
}) => {
  if (!hasModel(manufacturer, model)) return [];
  const models = carList[manufacturer];
  return models[model as keyof typeof models];
};

export const DependsForm: FC = () => {
  return (
    <Container as={Card} roundedTop={0}>
      <Heading>依存関係のあるフォーム</Heading>
      <Form schema={schema} display="contents">
        {({ field }) => (
          <>
            <SelectField label="メーカー" name={field.manufacturer.name} items={manufacturers} />
            <DependentSelectField
              label="車種"
              name={field.model.name}
              dependentFieldNames={["manufacturer"]}
              itemsSelector={modelItemsSelector}
            />
            <DependentSelectField
              label="グレード"
              name={field.grade.name}
              dependentFieldNames={["manufacturer", "model"]}
              itemsSelector={gradeItemsSelector}
            />
            <HStack justifyContent="space-between">
              <ResetButton />
              <Button>送信</Button>
            </HStack>
            <FormDebug />
          </>
        )}
      </Form>
      <Link
        external
        href="https://github.com/bmthd/react-experiment-repo/blob/master/src/views/dependent-form/index.tsx"
      >
        コード
      </Link>
    </Container>
  );
};