これは「react-jsonschema-formのカレンダー | Advent Calendar 2023 - Qiita」の5日目の記事です。
この記事では、react-jsonschema-form (RJSF) における、単一ファイル・複数ファイルのアップロードを試してみます。
目次
環境
- react-jsonschema-form 5.15.0
- React 18.2.0
- React Router 6.20.1
RJSFにて input="file" な項目を定義するには
このアドベントカレンダーの2日目の記事でふれていますが、
singleFile: { type: "string", format: "data-url" },
のように、
- type: "string"
- format: "data-url"
なpropertiesを定義します。
RJSFでは、ファイルアップロードについて
- 単一ファイル
- 複数ファイル
の両方に対応しているため、次はそれぞれを試してみます。
なお、アップロードするファイルですが、テキストファイルと画像ファイルを用意します。
テキストファイルは以下の文字列が含まれる、拡張子 txt
ファイルとします。
abc
単一ファイルのアップロード
ファイルアップロード後にformDataの値を確認する
RJSFのFormにある onSubmit
propsに割り当てる関数にて、 formData
の値を確認する処理を記載します。
import {RJSFSchema} from "@rjsf/utils"; import Form from "@rjsf/mui"; // MUI version import validator from "@rjsf/validator-ajv8"; export const SingleFileUpload = () => { const schema: RJSFSchema = { title: "Single File Upload", type: "object", required: [], properties: { singleFile: { type: "string", format: "data-url", } } } // onSubmitの引数の説明は以下 // https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props#onsubmit const onSubmit = async ({formData}, _event) => { console.log(formData) const singleFile = formData.singleFile console.log(singleFile) } return ( <div style={{width: "400px"}}> <Form schema={schema} validator={validator} onSubmit={onSubmit} /> </div> ) }
動かしてみます。
ダイアログでファイルを選択すると、以下のように表示されました。
続いてsubmitすると、console.logには
{singleFile: 'data:text/plain;name=2023_1205_upload.txt;base64,YWJj'}
と
data:text/plain;name=2023_1205_upload.txt;base64,YWJj
が表示されました。
dataUrlとは
上記でみたように、 formDataに dataUrl
が含まれていました。
dataUrl
については、MDNでは以下の解説がありました。
データ URL は data: スキームが先頭についた URL で、小さなファイルをインラインで文書に埋め込むことができます。
(略)
データ URL は接頭辞 (data:)、データの種類を示す MIME タイプ、テキストではないデータである場合のオプションである base64 トークン、データ自体の 4 つの部品で構成されます。
dataUrlからファイルの中身を取り出す
RJSFの onSubmit
で送信した内容はデータ自体に埋め込まれています。
そこで、以下のMDNの内容を参考に、データをデコードして読めるようにしてみます。
Converting arbitrary binary data | Base64 - MDN Web Docs Glossary: Definitions of Web-related terms | MDN
const onSubmit = async ({formData}, _event) => { console.log(formData) const singleFile = formData.singleFile // 追加 const data = await dataUrlToBytes(singleFile) console.log(data) } const dataUrlToBytes = async (dataUrl) => { // ファイルの中身を取り出す // https://developer.mozilla.org/en-US/docs/Glossary/Base64 const r = await fetch(dataUrl) const a = new Uint8Array(await r.arrayBuffer()) return new TextDecoder().decode(a) }
動かしてみると、consoleに abc
が表示されました。
ファイルの中身が取り出せたようです。
なお、画像ファイルをアップロードした場合は、以下のような人間には読めないものが表示されました。
dataUrlを元に、ファイルをブラウザ上に表示する
dataUrlに含まれるデータの中身ですが、
- img
- iframe
などのHTMLタグを使うことでブラウザ上に表示できます。
そこで、今回は iframe
を使い、
- formDataに含まれるファイルの中身を
useState
に保存する - コンポーネントでは
useState
で保存している中身を表示する
という実装を試してみます。
まずはstateを保存する部分です。
// stateを保持 const [data, setData] = useState("") const onSubmit = async ({formData}, _event) => { const singleFile = formData.singleFile // stateに保存 setData(singleFile) }
あとは、iframe
を使って、stateに保存されている内容を表示します。
return ( <div style={{width: "400px"}}> {data && (<div><iframe src={data} /></div>)} {data && (<div><a href={data} download={getFileName(data)}>ダウンロードリンク</a></div>)} <Form schema={schema} validator={validator} onSubmit={onSubmit} /> </div> )
実際に動かしてみます。
まずはテキストファイルをアップロードした結果です。ファイルの中身がiframeで表示されています。
続いて画像ファイルをアップロードします。こちらもiframeで表示されました。
dataUrlを元に、アップロードしたファイルをダウンロードできるようにする
dataUrlのデータをダウンロードすることも試してみます。
今回は以下とします。
iframe
で表示するときに使ったコンポーネントに対し、a
タグを使ったダウンロードリンクも追加- stateで保存している
data
はa
タグのsrc
に設定 - ダウンロード時のファイル名は
download
属性にて指定- RJSFの場合、ファイル名は dataUrl に含まれるため、そこから取り出す関数
getFileName
を用意
- RJSFの場合、ファイル名は dataUrl に含まれるため、そこから取り出す関数
方針に従った実装はこちら。
// ファイル名を取得する関数 const getFileName = (data: string) => { return data.split(";")[1].replace("name=", "") } return ( <div style={{width: "400px"}}> {data && (<div><iframe src={data} /></div>)} {data && (<div><a href={data} download={getFileName(data)}>ダウンロードリンク</a></div>)} <Form schema={schema} validator={validator} onSubmit={onSubmit} /> </div> )
ブラウザでファイルをsubmitしたところ、ダウンロードリンクが表示されました。
ダウンロードリンクをクリックすると、ダウンロード用のダイアログが開き、ファイル名が設定されていました。
そのままダウンロードを進めると、指定のディレクトリにファイルが保存されました。
以上で単数ファイルの挙動を確認できたため、次は複数ファイルの挙動を確認してみます。
複数ファイルアップロード
アップロード後のデータを確認する
続いて、複数ファイルアップロードを試してみます。
単数のときとの違いは、 properties
で type: "array"
を指定していることです。
import {RJSFSchema} from "@rjsf/utils"; import Form from "@rjsf/mui"; // MUI version import validator from "@rjsf/validator-ajv8"; export const MultipleFileUpload = () => { const schema: RJSFSchema = { title: "Multiple File Upload", type: "object", required: [], properties: { multipleFile: { type: "array", items: { type: "string", format: "data-url" } } } } // onSubmitの引数の説明は以下 // https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props#onsubmit const onSubmit = async ({formData}, _event) => { console.log(formData) const f = formData.singleFile console.log(f) } return ( <div style={{width: "400px"}}> <Form schema={schema} validator={validator} onSubmit={onSubmit} /> </div> ) }
では、複数ファイルをアップロードして動かしてみます。
Windowsの場合、ファイルを選択するダイアログでは1つのファイルのみ選択できます。そのため、ファイルの選択を繰り返すことで、複数ファイルアップロードができるようになります。
ちなみに、RJSFのデフォルトでは、選択したファイルを削除するようなボタンは見当たりませんでした。
続いて、submitしてconsoleを見ると、配列でdataUrlが生成されていることが分かりました。
なお、ファイルの中身を取り出したり、表示したりするのは単一ファイルと同じなため、ここでは省略します。
ソースコード
Githubにあげました。
https://github.com/thinkAmi-sandbox/rjsf_advent_calendar_2023
今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/rjsf_advent_calendar_2023/pull/4