これは「react-jsonschema-formのカレンダー | Advent Calendar 2023 - Qiita」の15日目の記事です。
大きいJSON SchemaをステップUI等で分割して入力させたいときのTipsをまとめてみました。
ステップやカテゴリごとにファイルを分ける方法もありますが、できるだけ汎用的に schema の変更を反映できる実装を意識しています。
分割しない場合
まずは分割せずにまとめて表示した際のサンプルです。
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Test Schema",
"type": "object",
"required": [],
"properties": {
"category1": {
"type": "object",
"properties": {
"item1": {
"type": "string"
},
"item2": {
"type": "string"
}
}
},
"category2": {
"type": "object",
"properties": {
"item3": {
"type": "string"
},
"item4": {
"type": "string"
}
}
}
}
}
import Form from '@rjsf/core'
import validator from '@rjsf/validator-ajv8';
import schema from './schema.json'
function App() {
return (
<div style={{marginLeft: '50px'}}>
<Form schema={schema} validator={validator}/>
</div>
)
}
export default App
schemaの分割
擬似的なステップUIとしてボタンを二つ置き、選択したカテゴリのFormを表示します。
JSON Schema から schema['properties'][category]
と中身を取り出し、それを RJSForm に渡すだけでカテゴリごとのFormの表示が可能です。
const [category, setCategory] = useState<'category1' | 'category2'>('category1');
return (
<div style={{marginLeft: '50px'}}>
<button onClick={() => setCategory('category1')}>Category1</button>
<button onClick={() => setCategory('category2')}>Category2</button>
<Form schema={schema['properties'][category]} validator={validator}/>
</div>
)
definitions を使っている場合
以下のJSON Schema のように definitions を使っている場合は注意が必要です。
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Test Schema",
"type": "object",
"required": [],
"definitions": {
"commonItem": {
"type": "number"
}
},
"properties": {
"category1": {
"type": "object",
"properties": {
"item1": {
"type": "string"
},
"item2": {
"type": "string"
},
"item3": {
"$ref": "#/definitions/commonItem"
}
}
},
"category2": {
"type": "object",
"properties": {
"item4": {
"type": "string"
},
"item5": {
"type": "string"
},
"item6": {
"$ref": "#/definitions/commonItem"
}
}
}
}
}
ここからカテゴリの Schema を取り出しそのまま RJSForm に渡しても、definitions が参照できずエラーが発生します。
Uncaught Error: Could not find a definition for #/definitions/commonItem.
この場合は、取り出したカテゴリの Schema に definitions をマージして RJSForm に渡す必要があります。
const [category, setCategory] = useState<'category1' | 'category2'>('category1');
const targetSchema = {
...schema['properties'][category],
definitions: schema['definitions']
}
return (
<div style={{marginLeft: '50px'}}>
<button onClick={() => setCategory('category1')}>Category1</button>
<button onClick={() => setCategory('category2')}>Category2</button>
<Form schema={targetSchema} validator={validator}/>
</div>
)
uiSchema を使っている場合
分割した schema の中で key が重複することがあるかと思います。
例えば以下のような例です。
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Test Schema",
"type": "object",
"required": [],
"properties": {
"category1": {
"type": "object",
"properties": {
"item1": {
"type": "string"
},
"duplicateItem": {
"type": "string"
}
}
},
"category2": {
"type": "object",
"properties": {
"item2": {
"type": "string"
},
"duplicateItem": {
"type": "string"
}
}
}
}
}
この例では duplicateItem
が重複していますが、 uiSchema を使って表示を調整したいときそのまま duplicateItem
を指定すると、どちらのカテゴリにも共通の設定がされます。
const uiSchema: UiSchema = {
duplicateItem: {
"ui:placeholder": "Ex: duplicate item"
}
}
uiSchema をネストさせカテゴリに応じて中身を取り出し RJSForm に渡すことで表示を切り替えることができます。
const uiSchema: UiSchema = {
category1: {
duplicateItem: {
"ui:placeholder": "Ex: duplicate item (c1)"
}
},
category2: {
duplicateItem: {
"ui:placeholder": "Ex: duplicate item (c2)"
}
}
}
function App() {
const [category, setCategory] = useState<'category1' | 'category2'>('category1');
return (
<div style={{marginLeft: '50px'}}>
<button onClick={() => setCategory('category1')}>Category1</button>
<button onClick={() => setCategory('category2')}>Category2</button>
<Form schema={schema['properties'][category]} validator={validator} uiSchema={uiSchema[category]}/>
</div>
)
}
formRef による Submit の発火
ステップUIなどの複雑なUIと組み合わせたときに、Form の外側から Submit 処理を制御したいことがあります。
その場合は、formRef を利用することで Submit 処理を発火させることができます。
const [category, setCategory] = useState<'category1' | 'category2'>('category1');
const formRef = useRef<Form>()
const handleSubmit = () => {
formRef.current?.submit()
}
return (
<div style={{marginLeft: '50px'}}>
<button onClick={() => setCategory('category1')}>Category1</button>
<button onClick={() => setCategory('category2')}>Category2</button>
<Form ref={formRef} schema={schema['properties'][category]} validator={validator} />
<button onClick={handleSubmit}>Submit by ref</button>
</div>
)