iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📝

Implementation Guide for Select with shadcn/ui and Conform

に公開

Please check out other articles in this series:

By combining these articles, you can gain comprehensive knowledge of form implementation using shadcn/ui and conform.

Overview

In this article, we will explain how to implement a basic Select using shadcn/ui and conform. We will introduce both the standard implementation method and a method using custom helper functions.

For the specifications and usage of the shadcn/ui Select component itself, please refer to the official website.

https://ui.shadcn.com/docs/components/select

Setup

First, import the necessary libraries.

import { useForm } from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { z } from 'zod'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '~/components/ui/select'
import { Label } from '~/components/ui/label'
import { getSelectProps, getSelectTriggerProps } from './helper'

The basic form structure is as follows:

export default function SelectForm() {
  const [form, fields] = useForm({
    onValidate: ({ formData }) => parseWithZod(formData, { schema }),
    constraint: getZodConstraint(schema),
    shouldValidate: 'onBlur',
    shouldRevalidate: 'onInput',
  })

  return (
    <form {...getFormProps(form)} method="post">
      {/* Select fields will be placed here */}
    </form>
  )
}

Schema Definition

const schema = z.object({
  basicSelect: z.enum(['apple', 'banana', 'orange'], {
    required_error: '選択してください',
    message: '無効な選択です',
  }),
})

Implementation Examples

1. Basic Select Implementation

<div>
  <Label htmlFor={fields.basicSelect.id}>果物を選択</Label>
  <Select
    key={fields.basicSelect.key}
    name={fields.basicSelect.name}
    defaultValue={fields.basicSelect.initialValue}
    onValueChange={(value) => {
      form.update({
        name: fields.basicSelect.name,
        value,
      })
    }}
  >
    <SelectTrigger 
      id={fields.basicSelect.id}
      aria-invalid={!fields.basicSelect.valid || undefined}
      aria-describedby={!fields.basicSelect.valid ? fields.basicSelect.errorId : undefined}
    >
      <SelectValue placeholder="選択してください" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="apple">りんご</SelectItem>
      <SelectItem value="banana">バナナ</SelectItem>
      <SelectItem value="orange">オレンジ</SelectItem>
    </SelectContent>
  </Select>
  <div id={fields.basicSelect.errorId} className="text-destructive">
    {fields.basicSelect.errors}
  </div>
</div>

2. Select Implementation using Helper Functions

You can implement it more concisely using the getSelectProps and getSelectTriggerProps functions defined in the helper.ts file.

<div>
  <Label htmlFor={fields.basicSelect.id}>果物を選択 (Helper使用)</Label>
  <Select
    {...getSelectProps(fields.basicSelect)}
    key={fields.basicSelect.key}
    onValueChange={(value) => {
      form.update({
        name: fields.basicSelect.name,
        value,
      })
    }}
  >
    <SelectTrigger {...getSelectTriggerProps(fields.basicSelect)}>
      <SelectValue placeholder="選択してください" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="apple">りんご</SelectItem>
      <SelectItem value="banana">バナナ</SelectItem>
      <SelectItem value="orange">オレンジ</SelectItem>
    </SelectContent>
  </Select>
  <div id={fields.basicSelect.errorId} className="text-destructive">
    {fields.basicSelect.errors}
  </div>
</div>

Explanation of Helper Functions

In the helper.ts file, two main functions are defined to simplify the Select implementation:

  1. getSelectProps: Generates the properties required for Select.
  2. getSelectTriggerProps: Generates the properties required for SelectTrigger.

By using these functions, you can reduce code redundancy and maintain a consistent implementation.

The implementation of helper.ts is as follows:

helper.ts
import type { FieldMetadata } from '@conform-to/react';

/**
 * Cleanup `undefined` from the result.
 * To minimize conflicts when merging with user defined props
 */
function simplify<Props>(props: Props): Props {
  for (const key in props) {
    if (props[key] === undefined) {
      delete props[key];
    }
  }
  return props;
}

export const getSelectProps = <Schema>(
  metadata: FieldMetadata<Schema>,
  options: {
    value?: boolean;
  } = {}
) => {
  const props: {
    key?: string;
    required?: boolean;
    name: string;
    defaultValue?: string;
  } = {
    key: metadata.key,
    required: metadata.required,
    name: metadata.name,
  };

  if (typeof options.value === 'undefined' || options.value) {
    props.defaultValue = metadata.initialValue?.toString();
  }

  return simplify(props);
};

export const getSelectTriggerProps = <Schema>(
  metadata: FieldMetadata<Schema>,
  options:
    | {
        ariaAttributes?: true;
        ariaInvalid?: 'errors' | 'allErrors';
        ariaDescribedBy?: string;
      }
    | {
        ariaAttributes: false;
      } = {
    ariaAttributes: true,
  }
) => {
  const props: {
    id: string;
    'aria-invalid'?: boolean;
    'aria-describedby'?: string;
  } = {
    id: metadata.id,
  };

  if (options.ariaAttributes) {
    const invalid =
      options.ariaInvalid === 'allErrors'
        ? !metadata.valid
        : typeof metadata.errors !== 'undefined';
    const ariaDescribedBy = options.ariaDescribedBy;
    props['aria-invalid'] = invalid || undefined;
    props['aria-describedby'] = invalid
      ? `${metadata.errorId} ${ariaDescribedBy ?? ''}`.trim()
      : ariaDescribedBy;
  }

  return simplify(props);
};

Debugging and Testing

You can add a debugging section to check the form values and errors:

<div>
  <h3>フォームの値</h3>
  <pre>{JSON.stringify(form.value, null, 2)}</pre>
</div>
<div>
  <h3>フォームのエラー</h3>
  <pre>{JSON.stringify(form.allErrors, null, 2)}</pre>
</div>

This allows you to easily check the state of the form.

Demo and Implementation Examples

If you would like to see the actual behavior of the content explained in this article, please visit the following demo page:

Form implementation demo using shadcn-ui and conform

On this demo page, you can see implementation examples for various form elements combining shadcn/ui and conform. You can check the behavior of each element, which is helpful for understanding the actual user experience.

Additionally, you can access the implementation source code from the demo page. By referring to the source code, you can see in detail how the implementation methods described in this article are applied.

By checking both the demo and the source code, you can deepen your understanding from both theoretical and practical perspectives. Please visit the demo page to see the actual behavior and code details.

Summary

By combining shadcn/ui and conform, you can easily implement a basic Select. Furthermore, using custom helper functions can make the code more concise and improve maintainability. By performing appropriate validation and error handling, you can create user-friendly selection forms.

In the next article, we will explain Checkbox implementation in detail. Stay tuned!

GitHubで編集を提案

Discussion