@rjsf/core
Version:
A simple React component capable of building HTML forms out of a JSON schema.
191 lines (190 loc) • 10.7 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, Component } from 'react';
import { ADDITIONAL_PROPERTY_FLAG, ANY_OF_KEY, descriptionId, getSchemaType, getTemplate, getUiOptions, ID_KEY, isFormDataAvailable, ONE_OF_KEY, resolveUiSchema, shouldRender, shouldRenderOptionalField, toFieldPathId, UI_OPTIONS_KEY, } from '@rjsf/utils';
import isObject from 'lodash-es/isObject.js';
import omit from 'lodash-es/omit.js';
/** The map of component type to FieldName */
const COMPONENT_TYPES = {
array: 'ArrayField',
boolean: 'BooleanField',
integer: 'NumberField',
number: 'NumberField',
object: 'ObjectField',
string: 'StringField',
null: 'NullField',
};
/** Computes and returns which `Field` implementation to return in order to render the field represented by the
* `schema`. The `uiOptions` are used to alter what potential `Field` implementation is actually returned. If no
* appropriate `Field` implementation can be found then a wrapper around `UnsupportedFieldTemplate` is used.
*
* @param schema - The schema from which to obtain the type
* @param uiOptions - The UI Options that may affect the component decision
* @param registry - The registry from which fields and templates are obtained
* @returns - The `Field` component that is used to render the actual field data
*/
function getFieldComponent(schema, uiOptions, registry) {
const field = uiOptions.field;
const { fields } = registry;
if (typeof field === 'function') {
return field;
}
if (typeof field === 'string' && field in fields) {
return fields[field];
}
const schemaType = getSchemaType(schema);
const type = Array.isArray(schemaType) ? schemaType[0] : schemaType || '';
const schemaId = schema.$id;
let componentName = COMPONENT_TYPES[type];
if (schemaId && schemaId in fields) {
componentName = schemaId;
}
// If the type is not defined and the schema uses 'anyOf' or 'oneOf', don't
// render a field and let the MultiSchemaField component handle the form display
if (!componentName && (schema.anyOf || schema.oneOf)) {
return () => null;
}
return componentName in fields ? fields[componentName] : fields['FallbackField'];
}
/** The `SchemaFieldRender` component is the work-horse of react-jsonschema-form, determining what kind of real field to
* render based on the `schema`, `uiSchema` and all the other props. It also deals with rendering the `anyOf` and
* `oneOf` fields.
*
* @param props - The `FieldProps` for this component
*/
function SchemaFieldRender(props) {
const { schema: _schema, fieldPathId, uiSchema: _uiSchema, formData, errorSchema, name, onChange, onKeyRename, onKeyRenameBlur, onRemoveProperty, required = false, registry, wasPropertyKeyModified = false, } = props;
const { schemaUtils, globalFormOptions, globalUiOptions, fields } = registry;
const { AnyOfField: _AnyOfField, OneOfField: _OneOfField } = fields;
const uiSchema = resolveUiSchema(_schema, _uiSchema, registry);
const uiOptions = getUiOptions(uiSchema, globalUiOptions);
const FieldTemplate = getTemplate('FieldTemplate', registry, uiOptions);
const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, uiOptions);
const FieldHelpTemplate = getTemplate('FieldHelpTemplate', registry, uiOptions);
const FieldErrorTemplate = getTemplate('FieldErrorTemplate', registry, uiOptions);
const schema = schemaUtils.retrieveSchema(_schema, formData);
const fieldId = fieldPathId[ID_KEY];
/** Intermediary `onChange` handler for field components that will inject the `id` of the current field into the
* `onChange` chain if it is not already being provided from a deeper level in the hierarchy
*/
const handleFieldComponentChange = useCallback((formData, path, newErrorSchema, id) => {
const theId = id || fieldId;
return onChange(formData, path, newErrorSchema, theId);
}, [fieldId, onChange]);
const FieldComponent = getFieldComponent(schema, uiOptions, registry);
const disabled = Boolean(uiOptions.disabled ?? props.disabled);
const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly));
const uiSchemaHideError = uiOptions.hideError;
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError);
const autofocus = Boolean(uiOptions.autofocus ?? props.autofocus);
if (Object.keys(schema).length === 0) {
return null;
}
let displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
/** If the schema `anyOf` or 'oneOf' can be rendered as a select control, don't render the selection and let
* `StringField` component handle rendering unless there is a field override and that field replaces the any or one of
*/
const isReplacingAnyOrOneOf = uiOptions.field && uiOptions.fieldReplacesAnyOrOneOf === true;
let XxxOfField;
let XxxOfOptions;
// When rendering the `XxxOfField` we'll need to change the fieldPathId of the main component, remembering the
// fieldPathId of the children for the ObjectField and ArrayField
let fieldPathIdProps = { fieldPathId };
if ((ANY_OF_KEY in schema || ONE_OF_KEY in schema) && !isReplacingAnyOrOneOf && !schemaUtils.isSelect(schema)) {
if (schema[ANY_OF_KEY]) {
XxxOfField = _AnyOfField;
XxxOfOptions = schema[ANY_OF_KEY].map((_schema) => schemaUtils.retrieveSchema(isObject(_schema) ? _schema : {}, formData));
}
else if (schema[ONE_OF_KEY]) {
XxxOfField = _OneOfField;
XxxOfOptions = schema[ONE_OF_KEY].map((_schema) => schemaUtils.retrieveSchema(isObject(_schema) ? _schema : {}, formData));
}
// When the anyOf/oneOf is an optional data control render AND it does not have form data, hide the label
const isOptionalRender = shouldRenderOptionalField(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(formData);
displayLabel = displayLabel && (!isOptionalRender || hasFormData);
fieldPathIdProps = {
childFieldPathId: fieldPathId,
// The main FieldComponent will add `XxxOf` onto the fieldPathId to avoid duplication with the rendering of the
// same FieldComponent by the `XxxOfField`
fieldPathId: toFieldPathId('XxxOf', globalFormOptions, fieldPathId),
};
}
const { __errors, ...fieldErrorSchema } = errorSchema || {};
// See #439: uiSchema: Don't pass consumed class names or style to child components
const fieldUiSchema = omit(uiSchema, ['ui:classNames', 'classNames', 'ui:style']);
if (UI_OPTIONS_KEY in fieldUiSchema) {
fieldUiSchema[UI_OPTIONS_KEY] = omit(fieldUiSchema[UI_OPTIONS_KEY], ['classNames', 'style']);
}
const field = (_jsx(FieldComponent, { ...props, onChange: handleFieldComponentChange, ...fieldPathIdProps, schema: schema, uiSchema: fieldUiSchema, disabled: disabled, readonly: readonly, hideError: hideError, autofocus: autofocus, errorSchema: fieldErrorSchema, rawErrors: __errors }));
const id = fieldPathId[ID_KEY];
// If this schema has a title defined, but the user has set a new key/label, retain their input.
let label;
if (wasPropertyKeyModified) {
label = name;
}
else {
label =
ADDITIONAL_PROPERTY_FLAG in schema
? name
: uiOptions.title || props.schema.title || schema.title || props.title || name;
}
const description = uiOptions.description || props.schema.description || schema.description || '';
const help = uiOptions.help;
const hidden = uiOptions.widget === 'hidden';
const classNames = ['rjsf-field', `rjsf-field-${getSchemaType(schema)}`];
if (!hideError && __errors && __errors.length > 0) {
classNames.push('rjsf-field-error');
}
if (uiOptions.classNames) {
classNames.push(uiOptions.classNames);
}
const helpComponent = (_jsx(FieldHelpTemplate, { help: help, fieldPathId: fieldPathId, schema: schema, uiSchema: uiSchema, hasErrors: !hideError && __errors && __errors.length > 0, registry: registry }));
/*
* AnyOf/OneOf errors handled by child schema
* unless it can be rendered as select control
*/
const errorsComponent = hideError || (XxxOfField && !schemaUtils.isSelect(schema)) ? undefined : (_jsx(FieldErrorTemplate, { errors: __errors, errorSchema: errorSchema, fieldPathId: fieldPathId, schema: schema, uiSchema: uiSchema, registry: registry }));
const fieldProps = {
description: (_jsx(DescriptionFieldTemplate, { id: descriptionId(id), description: description, schema: schema, uiSchema: uiSchema, registry: registry })),
rawDescription: description,
help: helpComponent,
rawHelp: typeof help === 'string' ? help : undefined,
errors: errorsComponent,
rawErrors: hideError ? undefined : __errors,
fieldPathId,
id,
label,
hidden,
onChange,
onKeyRename,
onKeyRenameBlur,
onRemoveProperty,
required,
disabled,
readonly,
hideError,
displayLabel,
classNames: classNames.join(' ').trim(),
style: uiOptions.style,
formData,
schema,
uiSchema,
registry,
};
return (_jsx(FieldTemplate, { ...fieldProps, children: _jsxs(_Fragment, { children: [field, XxxOfField && (_jsx(XxxOfField, { name: name, disabled: disabled, readonly: readonly, hideError: hideError, errorSchema: errorSchema, formData: formData, fieldPathId: fieldPathId, onBlur: props.onBlur, onChange: props.onChange, onFocus: props.onFocus, options: XxxOfOptions, registry: registry, required: required, schema: schema, uiSchema: uiSchema }))] }) }));
}
/** The `SchemaField` component determines whether it is necessary to rerender the component based on any props changes
* and if so, calls the `SchemaFieldRender` component with the props.
*/
class SchemaField extends Component {
shouldComponentUpdate(nextProps) {
const { registry: { globalFormOptions }, } = this.props;
const { experimental_componentUpdateStrategy = 'customDeep' } = globalFormOptions;
return shouldRender(this, nextProps, this.state, experimental_componentUpdateStrategy);
}
render() {
return _jsx(SchemaFieldRender, { ...this.props });
}
}
export default SchemaField;