UNPKG

@rjsf/core

Version:

A simple React component capable of building HTML forms out of a JSON schema.

191 lines (190 loc) 10.7 kB
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;