🩻

shadcn/uiのInput type=fileでプレビューを表示する

2024/06/05に公開

はじめに

shadcn/ui の Form コンポーネントでファイルアップロード機能を作成する際に、プレビューを表示させる手順になります。
https://ui.shadcn.com/docs/components/form

Form コンポーネントでは、簡単にフォームの UI を作成しながら React Hook Form と Zod を使用したバリデーションの実装が可能です。

完成物

コードを表示
"use client";

import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

import { useState, ChangeEvent } from "react";

function getImageData(event: ChangeEvent<HTMLInputElement>) {
  const dataTransfer = new DataTransfer();

  Array.from(event.target.files!).forEach((image) =>
    dataTransfer.items.add(image)
  );

  const files = dataTransfer.files;
  const displayUrl = URL.createObjectURL(event.target.files![0]);

  return { files, displayUrl };
}

export default function Page() {
  const [preview, setPreview] = useState("");

  return (
    <>
      <Form {...form}>
        <form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
          <FormField
            control={form.control}
            name="picture"
            render={({ field: { onChange, value, ...rest } }) => (
              <>
                <FormItem>
                  <FormLabel>ファイルアップロード</FormLabel>
                  <FormControl>
                    <Input
                      type="file"
                      {...rest}
                      onChange={(event) => {
                        const { files, displayUrl } = getImageData(event);
                        setPreview(displayUrl);
                        onChange(files);
                      }}
                    />
                  </FormControl>
                  <FormDescription>ファイルを選択してください</FormDescription>
                  <FormMessage />
                </FormItem>
              </>
            )}
          />
          <Button type="submit">送信</Button>
        </form>
      </Form>
      <div className="aspect-video max-w-[560px]">
        {preview ? (
          <img
            src={preview}
            alt=""
            className="w-full h-full object-contain object-center"
          />
        ) : (
          <div className="w-full h-full bg-background/70 rounded-lg border flex justify-center items-center">
            <Image size={100} color="gray" />
          </div>
        )}
      </div>
    </>
  );
}

※バリデーション設定等については省いています。

実装方法

shadcn/ui の Input コンポーネントでファイルアップロードのフィールドを作成し、Javascript のURL.createObjectURLでオブジェクト URL を生成。
useState を用いてその URL を取得して表示させます。

ファイルアップロードのフィールドを作成する

全体像
<Form {...form}>
  <form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
    <FormField
      control={form.control}
      name="picture"
      render={({ field: { onChange, value, ...rest } }) => (
        <>
          <FormItem>
            <FormLabel>ファイルアップロード</FormLabel>
            <FormControl>
              <Input
                type="file"
                {...rest}
                onChange={(event) => {
                  const { files, displayUrl } = getImageData(event);
                  setPreview(displayUrl);
                  onChange(files);
                }}
              />
            </FormControl>
            <FormDescription>ファイルを選択してください</FormDescription>
            <FormMessage />
          </FormItem>
        </>
      )}
    />
    <Button type="submit">送信</Button>
  </form>
</Form>

プレビュー機能に関係してくる箇所は主に以下になります。

<FormField
  control={form.control}
  name="picture"
  render={({ field: { onChange, value, ...rest } }) => (

renderプロパティにonChangevalueを指定することでフィールドの値が変更された時に特定の機能を与えることが可能になります。
...restにはその他のプロパティが保持されます。

<Input
  type="file"
  {...rest}
  onChange={(event) => {
    const { files, displayUrl } = getImageData(event);
    setPreview(displayUrl);
    onChange(files);
  }}
/>

onChangeの中で後述のオブジェクト URL を生成し state を更新する処理を行います。

オブジェクト URL を生成する

function getImageData(event: ChangeEvent<HTMLInputElement>) {
  const dataTransfer = new DataTransfer();

  Array.from(event.target.files!).forEach((image) =>
    dataTransfer.items.add(image)
  );

  const files = dataTransfer.files;
  const displayUrl = URL.createObjectURL(event.target.files![0]);

  return { files, displayUrl };
}

Input の onChange の中で引数を渡してこの関数が実行されています。
filesにアップロードされたローカルファイルを保持し、URL.createObjectURLを使用してオブジェクト URL を生成してdisplayUrlに代入しています。

プレビューを表示する

const [preview, setPreview] = useState("");
~~~
~~~
<Input
  ~~~
  onChange={(event) => {
    const { files, displayUrl} = getImageData(event)
    setPreview(displayUrl);
  }}
/>

プレビュー用の statepreviewを用意します。
ファイルがアップロードされgetImageDataが実行されるたびにdisplayUrlが代入されます。

<div className="aspect-video max-w-[560px]">
  {preview ? (
    <img
      src={preview}
      alt=""
      className="w-full h-full object-contain object-center"
    />
  ) : (
    <div className="w-full h-full bg-background/70 rounded-lg border flex justify-center items-center">
      <Image size={100} color="gray" />
    </div>
  )}
</div>

previewにはオブジェクト URL が返ってくるので img タグの src にpreviewを表示すればプレビューが表示されます。

参考

https://github.com/shadcn-ui/ui/issues/250

Discussion