見出し画像

Reactで画像入力フォームをカスタマイズする(react-hook-form対応)

Webサイトでファイルの入力を受け取りたいとき、input要素のtype属性に"file"を指定することで実現できます。しかし、これだけではデフォルトのスタイルが適用されてしまい、サイト全体のイメージに合わせるのが難しくなります。そこで、この記事ではカスタマイズ可能な画像用のファイル送信フォームの例を紹介し、実装のためのヒントを解説していきます。

✍️

伊藤忠テクノソリューションズ株式会社
BUILDサービス部 ソフトウェアエンジニア
板倉翔太

概要

  • Reactを使用する (ただし考え方は Vanilla JS でも使えます)

  • TypeScriptを使用する(ただし考え方はJavaScriptでも使えます)

  • react-hook-formに対応する

  • 「カメラを使用」と「ファイル選択」のボタンを分ける

  • 「カメラを使用」はモバイル端末のみで有効にする

  • ファイル名を表示する

  • 画像のプレビューを表示する

作成イメージ

サンプルコード

コードの全文はGitHubに載せています。

考え方

機能の分離

通常のファイル入力フォームでは、下記の例のように、ファイル名の表示と入力ボタンが一緒になっている例がほとんどです。しかし、これらの機能は分離して表示することが可能です。

ファイル入力フォームのデフォルトのスタイル

具体的には、input要素のデフォルトのデザインを`display: none;`で非表示にしてしまい、clickイベントを別のボタンに割り当てるなど、TypeScriptで制御します。ファイル名は入力されたファイルから取得します。

プレビューの表示

ユーザーが入力した画像のプレビューを表示するため、onChangeイベントの中で、フォームに入力されたファイルを文字列に変換します。そして、その文字列をimg要素のsrc属性に設定することでプレビューを実現します。

実装の個別Tips

要素の取得

TypeScriptでフォームを制御するためにinput要素をオブジェクトとして取得する必要があります。Vanilla JS で言えばidを使う場面です。

let inputElement = document.getElementById("fileInput");
<input type="file" id="fileInput" />

これをReactで実現したいときは、refを使います。

const fileInput = useRef<HTMLInputElement | null>(null);
<input type="file" ref={fileInput} />

これでTypeScriptからclickイベントを発火させることができるようになりました。

if (fileInputRef.current) {
  fileInput.current.click();
}

react-hook-formに対応

Reactでフォームの制御を行う際に定番のreact-hook-formですが、react-hook-formでもinput要素の内容を収集するためにrefが使われています。

何も考えずにref属性をinputに指定してしまうと、react-hook-formのuseFormで作られるrefと、自分で作成したrefが干渉してしまうので、少し工夫が必要です。react-hook-formの公式にその解決策が書かれていました。

const fileInput = useRef<HTMLInputElement | null>(null);
const { register } = useForm();
const { ref, ...rest } = register("file", {
  required: "ファイルを選択してください",
});
<input
  type="file"
  ref={(e) => {
    ref(e);
    fileInput.current = e;
  }}
  {...rest}
/>

「カメラを使用」「ファイル選択」ボタンを作成

ファイルを入力するinput要素には、ファイルの種別を指定するaccept属性があります。特に、accept属性が画像や動画などの場合、capture属性を使用することができます。capture属性を指定することで、クリックした時の動作が変わります。

  • environment: 外向きカメラ

  • user: 内向きカメラ

  • 無指定: デフォルトの動作。普通はファイルピッカー

 <input type="file" capture="user" accept="image/*" />

この場合、accept属性で"image/*"を指定しており、capture属性で"user"を指定しています。モバイル端末でこのinput要素をクリックした場合、内向きカメラが起動して画像を撮影することができます。これによって「カメラで撮影」ボタンなどを作成できます。

const camera = () => {
  if (!fileInput.current) return;
  fileInput.current.setAttribute("capture", "environment");
  fileInput.current.click();
};
<button onClick={camera} type="button">
  カメラで撮影
</button>

モバイル判定

input要素のcapture属性は、モバイル端末でのみ有効であり、デスクトップPCなどの非対応な環境では指定しても無視されます。しかし、ボタンを出し分けする際などに、事前にモバイル端末かどうかを把握したい場合もあるでしょう。今回の例では、以下のような方法で実現しました。

const isMobile = window.navigator.userAgent.toLowerCase().includes("mobile");
<button onClick={camera} type="button" disabled={!isMobile}>
  カメラで撮影
</button>

onChange

ユーザーが入力した際にプレビューやファイル名を表示するために、onChangeに処理を追加します。react-hook-formが生成するonChangeと干渉しないようにするために、registerの引数の中に独自に生成したonChangeを指定します。

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const files = e.target.files;
  if (!files || files.length <= 0) return;
  deployment(files);  // ファイル名とプレビューの表示
};
const { ref, ...rest } = register("file", {
  onChange,
  required: "ファイルを選択してください",
});
<input
  type="file"
  ref={(e) => {
    ref(e);
    fileInput.current = e;
  }}
  accept="image/*"
  style={{ display: "none" }}
  {...rest}
/>

ファイル名とプレビューの表示

ファイル名と文字列変換されたファイル保持用のstateを用意。

const [fileName, setFileName] = useState("");
const [imageData, setImageData] = useState("");

`FileReader`を使って、ファイルが読み込まれたらstateを更新します。

const deployment = (files: FileList) => {
  const file = files[0];
  const fileReader = new FileReader();
  setFileName(file.name);
  fileReader.onload = () => {
    setImageData(fileReader.result as string);
  };
  fileReader.readAsDataURL(file);
};

これをimg要素のsrc属性に入れることで画像が表示されます。

<img src={imageData} />
<div>{fileName}</div>

Hook化

ここまで、細かなTipsを説明してきましたが、複数の画面でファイル入力する際に毎回このようなコードを書くのは面倒です。そこで、Hookを使用すると便利かもしれません。サンプルコードにはHookを使った例も作成しましたので、参考にしてみてください。