技术/前端/React

最近在调研react-jsonschema-form,想通过这个来简化form表单的开发,另外是想通过配置化的方式来快速生成不同表单项。但是经过调研发现,rjsf本身是用bootstrap-v3来渲染的,不过它也提供了withTheme的方式来重写或者定制widgets/fields,也可以覆盖默认表单组件的props。

rjsf-demo.gif

使用

官方示例:

  1. import React, { Component } from 'react';
  2. import { withTheme } from 'react-jsonschema-form';
  3. // demo theme
  4. import Bootstrap4Theme from 'react-jsonschema-form-theme-bs4';
  5. const ThemedForm = withTheme(Bootstrap4Theme);
  6. // use ThemedForm
  7. class Demo extends Component {
  8. render() {
  9. return <ThemedForm {...props}/>
  10. }
  11. }

高级自定义方案介绍

react-jsonschema-form提供了不同方式的定制方法,可以针对不同场景来实现定制,具体应用参考下表:

rjsf-定制方法-整理表格.jpg

Theme参数

withTheme是一个高阶组件,并且接收唯一一个对象参数(ThemeObj),该组件会返回一个经过主题改变后的组件来替换rjsf提供的标准Form组件。

首先,ThemeObj的属性和标准Form组件的属性一样,定制后的Form组件会将ThemeObj对象属性和标准Form组件的默认属性合并,如widgetsfields

如果参入的属性名称与默认的同名,则会将会属性合并,如果不指定widget或者field则会使用默认的。

Widgets & fields

  • widget: 代表一个HTML标签,用于用户输入数据,如inputselect
  • field: 通过使用一个或多个widget组合来处理字段内部状态,可以理解为一个field代表表单中的一项

所以,如果我们想要覆盖标准的日期输入框,则可以自定义一个DateWidget,然后传给ThemeObj,而Field也是一样。

const MyCustomDateWidget = (props) => {
  return (
    <Datepicker
      className="custom"
      value={props.value}
      required={props.required}
      onChange={(event) => props.onChange(event.target.value)} />
  );
};

const myWidgets = {
  myCustomWidget: MyCustomDateWidget
};

const ThemeObject = {widgets: myWidgets};
export default ThemeObject;

Widgets

一个widget可以通过uiSchema来指定数据类型,包括:

  • string
  • number
  • integer
  • boolean

任何定制化的widget组件都会接收如下属性:

  • id: 该字段的id(自动生成)
  • schema: 当前字段的schema对象(局部)
  • value: 当前字段的值
  • placeholder: 可选,占位符
  • required: 是否必须
  • disabled: 是否disabled,true/false
  • readonly: 是否只读
  • autofocus: 是否默认autofocus
  • onChange: change事件回调
  • onBlur: blur事件回调,参数: (id, value)
  • onFocus: focus事件回调,参数:(id, value)
  • options: 组件额外参数,通过uiSchema传递或widget的defaultOptions
  • formContext: 传递给Form组件的formContext对象属性,会传递到自定义的widget

可以自定义的widgets列表如下:

  • AltDateTimeWidget
  • AltDateWidget
  • CheckboxesWidget
  • CheckboxWidget
  • ColorWidget
  • DateTimeWidget
  • DateWidget
  • EmailWidget
  • FileWidget
  • HiddenWidget
  • PasswordWidget
  • RadioWidget
  • RangeWidget
  • SelectWidget
  • TextareaWidget
  • TextWidget
  • UpDownWidget
  • URLWidget

可参考源码了解各个Widget的实现:https://github.com/rjsf-team/react-jsonschema-form/tree/master/packages/core/src/components/widgets

Fields

我们可以通过给uiSchema指定ui:field属性来实现任何自定义的field组件,比如可以创建一个并且注册一个geo组件来处理latitudelongitude表单项

// 定义schema
const schema = {
  type: "object",
  required: ["lat", "lon"],
  properties: {
    lat: {type: "number"},
    lon: {type: "number"}
  }
};
// Define a custom component for handling the root position object
class GeoPosition extends React.Component {
  constructor(props) {
    super(props);
    this.state = {...props.formData};
  }

  onChange(name) {
    return (event) => {
      this.setState({
        [name]: parseFloat(event.target.value)
      }, () => this.props.onChange(this.state));
    };
  }

  render() {
    const {lat, lon} = this.state;
    return (
      <div>
        <input type="number" value={lat} onChange={this.onChange("lat")} />
        <input type="number" value={lon} onChange={this.onChange("lon")} />
      </div>
    );
  }
}
// Define the custom field component to use for the root object
const uiSchema = {"ui:field": "geo"};
// Define the custom field components to register; here our "geo"
// custom field component
const fields = {geo: GeoPosition};
render((
  <Form
    schema={schema}
    uiSchema={uiSchema}
    fields={fields} />
), document.getElementById("app"));

上面代码逻辑:

  • 定义jsonschema,指定字段名称及其他配置
  • 定义一个field组件处理对应字段
  • 指定ui:field交给geo处理
  • 注册geo为一个field
    注意: 注册过的字段可以整个schema中使用

Field会接收如下参数:

  • schema: 当前field的son schema
  • uiSchema:当前field的uiSchema
  • idSchema: 子field的标识id树,针对ArrayField使用
  • formData: 该字段的数据
  • errorSchema: 错误提示(当前字段及子字段),针对ArrayField使用
  • registry: 包含所有注册过的widgets和fields、definitions和formContext
  • formContext: 该属性会自动传递给所有的fields和widgets

可以自定义的fields列表如下:

  • ArrayField
  • BooleanField
  • DescriptionField
  • MultiSchemaField
  • NullField
  • NumberField
  • ObjectField
  • SchemaField
  • StringField
  • TitleField
  • UnsupportedField

模板

通过自定义模板,我们可以改变单个或多个表单项的布局,另外可以基于这种能力,我们可以更好地控制表单的布局,通过参数来调整表单的布局,如指定表单水平方向或垂直方向布局。

Field模板

Field Tempalte赋予我们控制各个field内部(即每个表单项)的布局能力,通常一个field template基本是一个React 无状态组件,接收field相关的props,从而可以重新组织单个表单项的内部结构

定义一个CustomFieldTemplate:

function CustomFieldTemplate(props) {
  const {id, classNames, label, help, required, description, errors, children} = props;
  return (
    <div className={classNames}>
      <label htmlFor={id}>{label}{required ? "*" : null}</label>
      {description}
      {children}
      {errors}
      {help}
    </div>
  );
}

上面看出我们改变了labeldescriptionchildrenerrors以及help的布局,当然我们也可以实现其他形式。另外,针对descriptionerrorshelp,我们也可以使用rawDescriptionrawErrorsrawHelp来控制元素的渲染过程。

应用方法1:

<Form schema={schema} FieldTemplate={CustomFieldTemplate}/

应用方法2:

const uiSchema = {
    "ui:FieldTemplate": CustomFieldTemplate
}

<Form schema={schema} uiSchema={uiSchema}/>

应用方法3:

import { withTheme } from 'react-jsonschema-form'
const Theme = {
    FieldTemplate: CustomFieldTemplate
}
const Form = withTheme(Theme)

自定义的field template接收如下参数:
https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#custom-widgets-and-fields

  • id: 当前field的id,可用来渲染指定该id的label
  • classNames: 默认是Bootstrap Css 类名字符串,也会将uiSchema中自定义的classNames合并进行来
  • label: 该field对应的label字符串
  • description: 该field的description渲染后的组件实例(如果有自定义的DescriptionField)
  • rawDescription: ui:description定义的字符串
  • children: 该field的field或widget组件实例
  • errors: 该字段的错误提示信息的组件实例列表
  • rawErrors: 该字段的原始错误提示信息列表
  • help: ui:help定义的帮助信息组件实例
  • rawHelp: ui:help定义的原始帮助信息字符串
  • hidden: 是否隐藏该field
  • required: 是否必须
  • readonly: 是否只读
  • disabled: 是否disabled
  • displayLabel: 是否渲染label,在多个field嵌套时可以根据该属性来控制label是否渲染
  • fields: 所有默认和自定义的fields
  • schema: 该字段的schema对象
  • uiSchema: 该对象的uiSchema对象
  • formContext: Form组件的formContext属性

一个Form的全局field template只能有一个,但是可以通过ui:FieldTemplate的方式指定不同的field template

Array Field Template

FieldTemplate类似,通过ArrayFieldTemplate可以定制数组项目的布局。
实现一个自定义布局的CustomArrayFieldTemplate:

function ArrayFieldTemplate(props) {
  return (
    <div>
      {props.items.map(element => element.children)}
      {props.canAdd && <button type="button" onClick={props.onAddClick}></button>}
    </div>
  );
}

应用方案可参考FieldTemplate
每个ArrayFieldTemplate会接收如下属性:

  • DescriptionField: 注册的DescriptionField,在需要的地方可以使用
  • TitleField: 注册的TitleField,在需要的地方可以使用
  • canAdd: 是否可以新增数组元素
  • className: className字符串
  • disabled: 是否disabled
  • idSchema: 对象
  • items: 数组项,每个元素为一个对象(具体格式如下)
  • onAddClick: (event) => void: 新增数组项事件回调
  • readonly: 是否只读
  • required: 是否必须
  • schema: 该数组的schema对象
  • uiSchema: 该数组的uiSchema对象
  • title: 该数组field的标题
  • formContex: 全局formContext
  • formData: 该数组的表单数据

上面的items属性,其每个元素包括如下属性:

  • children: item的html内容
  • className: className字符串
  • disabled: 是否disabled
  • hasMoveDown: 是否可向下移动
  • hasMoveUp: 是否可向上移动
  • hasToolbar: 是否有工具条
  • index: 数组下标
  • key: 该item的key
  • onAddIndexClick: (index) => (event) => void: 新增一个数组项回调
  • onDropIndexClick: (index) => (event) => void: 删除一哥数组项回调
  • onReorderClick: (index, newIndex) => (event) => void : 交换两个数组项回调
  • readonly: 是否只读

注意: 无论是ArrayFieldTemplate还是ObjectFieldTemplate,都是在FieldTemplate内渲染的,因此想要定制ArrayFieldTemplateObjectFieldTemplate,应该先实现FieldTemplate

Object Field Template

FieldTemplate类似,ObjectFieldTemplate用来组织对象格式的渲染。

function ObjectFieldTemplate(props) {
  return (
    <div>
      {props.title}
      {props.description}
      {props.properties.map(element => <div className="property-wrapper">{element.content}</div>)}
    </div>
  );
}

应该方式参考FieldTemplate
每个ObjectFieldTemplate可接收如下参数:

  • DescriptionField: 注册的DescriptionField,在需要的地方可以使用
  • TitleField: 注册的TitleField,在需要的地方可以使用
  • properties: 对象的属性数组,具体个数如下
  • disabled: 是否disabled
  • readonly: 是否只读
  • required: 是否必须
  • schema: 该数组的schema对象
  • idSchema: 对象
  • uiSchema: 该数组的uiSchema对象
  • title: 该对象field的标题
  • description: 该对象field的描述
  • formContex: 全局formContext
  • formData: 该对象field的表单数据

properties的每项:

  • content: 当前项的html内容
  • name: 当前项的属性名称
  • disabled: 是否disabled
  • readonly: 是否只读

ErrorListTemplate

想要控制表单错误内容如何显示,可以定义一个error list template,请了解该错误列表是全局的,会显示在你的表单之上,但我们也可以通过具体参数来控制是否显示错误列表。

我们可以定义个无状态组件并且接收errors数组属性,然后将errors渲染出来:

function ErrorListTemplate(props) {
  const {errors} = props;
  return (
    <ul>
      {errors.map(error => (
          <li key={error.stack}>
            {error.stack}
          </li>
        ))}
    </ul>
  );
}

应用方式同样参考FieldTemplate

另外,可以指定showErrorList来控制是否显示ErrorListTemplate

const Theme = {
    ErrorList={ErrorListTemplate}
    showErrorList: false
}

ErrorListTemplate除了接收errors属性外,还接收以下参数:

  • errorSchema: 由Form组件生成
  • schema: 传给Form的schema
  • uiSchema: 传给Form的uiSchema
  • formContext: 传给Form的formContext

定制Antd主题

通过提供antd版本的widgets和fields来实现定制,看下项目目录:

rjfs-theme-structure.jpg

针对不同的field或widget可以通过重新定义组件来实现定制化,如CheckboxWidget

import React from 'react';

import { Checkbox } from 'antd';
import { WidgetProps } from 'react-jsonschema-form';

const CheckboxWidget = (props: WidgetProps) => {
  const {
    id,
    value,
    required,
    disabled,
    readonly,
    label,
    autofocus,
    onChange,
    onBlur,
    onFocus,
  } = props;

  const _onChange = ({
    target: { value },
  }: React.ChangeEvent<HTMLInputElement>) => onChange(value);
  const _onBlur = ({
    target: { value },
  }: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value);
  const _onFocus = ({
    target: { value },
  }: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value);

  return (
    <Checkbox
      id={id}
      checked={typeof value === 'undefined' ? false : value}
      required={required}
      disabled={disabled || readonly}
      autoFocus={autofocus}
      onChange={_onChange}
      onBlur={_onBlur}
      onFocus={_onFocus}
    >{label}</Checkbox>
  );
};

export default CheckboxWidget;

使用withTheme

import FieldTemplate from '../Templates/FieldTemplate';
*// import ArrayFieldTemplate from '../Templates/ArrayFieldTemplate';*
import ObjectFieldTemplate from '../Templates/ObjectFieldTemplate';
import ErrorListTemplate from '../Templates/ErrorListTemplate';
import Fields from '../Fields';
import Widgets from '../Widgets';

import { ThemeProps } from 'react-jsonschema-form';
// 默认提供的fields及widgets
import { getDefaultRegistry } from 'react-jsonschema-form/lib/utils';
const { fields, widgets } = getDefaultRegistry();

const Theme: ThemeProps = {
  fields: { ...fields, ...Fields },
  widgets: { ...widgets, ...Widgets },
  FieldTemplate,
  *// ArrayFieldTemplate,*
  ObjectFieldTemplate,
  ErrorList: ErrorListTemplate,
};

export default Theme;

当然,上面的代码目前仅仅刚跑起来,等后面优化完会放出的哦。记得关注我的公号【也寻常】,顺便聊点别的~
qrcode_for_gh_3185024eec7a_258.jpg