react-jsonschema-formにて、Fieldをカスタマイズしてみる

これは「react-jsonschema-formのカレンダー | Advent Calendar 2023 - Qiita」の8日目の記事です。

7日目の記事では、react-jsonschema-form (RJSF) のコンポーネントのうち Widget についてカスタマイズしてみましたので、今回はFieldをカスタマイズしてみます。
Custom Templates | react-jsonschema-form

 
なお、Widgetのカスタマイズは

Overrides just the input box (not layout, labels, or help, or validation)

でしたが、Fieldのカスタマイズは

Overrides all behaviour

とあり、Widgetに比べて色々できます。

 
目次

 

環境

  • react-jsonschema-form 5.15.0
  • React 18.2.0
  • React Router 6.20.1

 

公式ドキュメントのCustom Fieldを関数ベースコンポーネントで実装

公式ドキュメントでは、Custom Fieldがクラスベースコンポーネントで書かれています。

そこで、これを関数ベースで書いていきます。

 
まずはFieldを用意します。Fieldでは

  • formData をstateで管理する
  • onChangeonBlurformData を更新する

とします。

なお、inputタグの value にstateの値を割り当てると

A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components

というWarningが表示されるため、ここでは value なしにします。

import {FieldProps} from "@rjsf/utils";
import {useState} from "react";

export const GeoField = ({formData, onChange}: FieldProps) => {
  const [state, setState] = useState({ ...formData });

  const handleInputChange = (name: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
    setState({
      ...state,
      [name]: parseFloat(event.target.value),
    })
  }

  const handleComponentChange = () => {
    onChange(state)
  }

  return (
    <div>
      <input type="number" onChange={handleInputChange("lat")} />
      <input type="number" onChange={handleInputChange("lon")} onBlur={handleComponentChange} />
    </div>
  )
}

 
次に、Formを使うコンポーネントを用意します。

JSON Schemaでは latlon を定義します。

const schema: RJSFSchema = {
  title: "Geo Field Form",
  type: "object",
  required: [],
  properties: {
    lat: {
      type: "number",
    },
    lon: {
      type: "number"
    }
  }
}

 
続いて、RegistryFieldsType 型のオブジェクトにて、fieldの設定を行います。

const fields: RegistryFieldsType  = {
  geo: GeoField
}

 
また、uiSchemaでは ui:field キーを使い、値にRegistryFieldsType型のオブジェクトのキーを設定します。

Widgetのときと異なり、ルートに ui:widget を設定しています。

const uiSchema: UiSchema = {
  "ui:field": "geo",
}

 
最後にFormを用意し、 fields props にRegistryFieldsType型のオブジェクトを渡します。

const onSubmit = ({formData}, _event) => {
  console.log(formData)
}

return (
  <div style={{width: '400px'}}>
    <Form
      schema={schema}
      uiSchema={uiSchema}
      fields={fields}
      validator={validator}
      onSubmit={onSubmit}
    />
  </div>
)

 
実際に動かしてみると、latとlonの入力項目を持つフォームが表示されました。

また、入力してからsubmitすると、consoleに値が出力されました。

 

EnumをList表示しつつ、入力欄も持つCustom Fieldを実装

もう一つ実装例を示します。

const schema: RJSFSchema = {
  title: "Enum List Field",
  type: "object",
  required: [],
  properties: {
    enumLabelWithOneOf: {
      type: "string",
      oneOf: [
        { "const": "foo", title: "foo label" },
        { "const": "bar", title: "bar label" },
        { "const": "baz", title: "baz label" },
      ]
    },
    stringInput: {
      type: "string",
    }
  }
}

というJSON Schemaがあったときに、

  • enumLabelWithOneOfというEnumの内容はListで表示
  • stringInputは入力項目

というCustom Fieldを用意してみます。

 
まずはFieldの定義です。

先ほどの例と異なるのは、FieldPropsオブジェクトの中から JSON Schema の定義schema.properties['enumLabelWithOneOf'].oneOf を取り出し、Listを作っているところです。

なお、FieldPropsに含まれる内容については、公式ドキュメントの以下のページに記載があります。
https://rjsf-team.github.io/react-jsonschema-form/docs/advanced-customization/custom-widgets-fields/#field-props

import {FieldProps} from "@rjsf/utils";
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';

export const EnumListField = ({schema, formData, onChange}: FieldProps) => {
  const handleChange = (name: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
    onChange({
      ...formData,
      [name]: event.target.value,
    })
  }

  return (
    <>
      <List>
        {schema.properties['enumLabelWithOneOf'].oneOf.map((f, i) => {
          return (
            <ListItem key={i}>
              <ListItemText primary={f.title} secondary={f.const} />
            </ListItem>
          )})
        }
      </List>

      <input type="text" onChange={handleChange('stringInput')} />
    </>
  )
}

 
Formを使うコンポーネントの定義については、schemaの定義以外はGeoFieldFormのものと同じ形となります。

import {RegistryFieldsType, RJSFSchema, UiSchema} from "@rjsf/utils";
import Form from "@rjsf/mui";
import validator from "@rjsf/validator-ajv8";
import {EnumListField} from "./fields/EnumListField";

export const EnumListFieldForm = () => {
  const schema: RJSFSchema = {
    title: "Enum List Field",
    type: "object",
    required: [],
    properties: {
      enumLabelWithOneOf: {
        type: "string",
        oneOf: [
          { "const": "foo", title: "foo label" },
          { "const": "bar", title: "bar label" },
          { "const": "baz", title: "baz label" },
        ]
      },
      stringInput: {
        type: "string",
      }
    }
  }

  const uiSchema: UiSchema = {
    "ui:field": "enumList",
  }

  const fields: RegistryFieldsType  = {
    enumList: EnumListField
  }

  // onSubmitの引数の説明は以下
  // https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props#onsubmit
  const onSubmit = ({formData}, _event) => {
    console.log(formData)
  }

  return (
    <div style={{width: '400px'}}>
      <Form
        schema={schema}
        uiSchema={uiSchema}
        fields={fields}
        validator={validator}
        onSubmit={onSubmit}
      />
    </div>
  )
}

 
動かしてみると、JSON Schemaの

  • enumLabelWithOneOf はList表示
  • stringInput はinput表示

となっています。

また、inputに入力してsubmitすると、consoleに入力値が出力されます。

 
このように、Custom Fieldを使うことで、複数の properties を使った表示ができます。

 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/rjsf_advent_calendar_2023

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/rjsf_advent_calendar_2023/pull/7