UNPKG

@rjsf/core

Version:

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

175 lines (174 loc) 10.5 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, Component } from 'react'; import { ADDITIONAL_PROPERTY_FLAG, deepEquals, descriptionId, getSchemaType, getTemplate, getUiOptions, ID_KEY, mergeObjects, TranslatableString, UI_OPTIONS_KEY, } from '@rjsf/utils'; import isObject from 'lodash-es/isObject.js'; import omit from 'lodash-es/omit.js'; import Markdown from 'markdown-to-jsx'; /** 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 idSchema - The id that is passed to the `UnsupportedFieldTemplate` * @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, idSchema, registry) { const field = uiOptions.field; const { fields, translateString } = 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] : () => { const UnsupportedFieldTemplate = getTemplate('UnsupportedFieldTemplate', registry, uiOptions); return (_jsx(UnsupportedFieldTemplate, { schema: schema, idSchema: idSchema, reason: translateString(TranslatableString.UnknownFieldType, [String(schema.type)]), registry: registry })); }; } /** 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, idSchema: _idSchema, uiSchema, formData, errorSchema, idPrefix, idSeparator, name, onChange, onKeyChange, onDropPropertyClick, required, registry, wasPropertyKeyModified = false, } = props; const { formContext, schemaUtils, globalUiOptions } = 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 = _idSchema[ID_KEY]; const idSchema = mergeObjects(schemaUtils.toIdSchema(schema, fieldId, formData, idPrefix, idSeparator), _idSchema); /** 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, newErrorSchema, id) => { const theId = id || fieldId; return onChange(formData, newErrorSchema, theId); }, [fieldId, onChange]); const FieldComponent = getFieldComponent(schema, uiOptions, idSchema, 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; } const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); 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, idSchema: idSchema, schema: schema, uiSchema: fieldUiSchema, disabled: disabled, readonly: readonly, hideError: hideError, autofocus: autofocus, errorSchema: fieldErrorSchema, formContext: formContext, rawErrors: __errors })); const id = idSchema[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 richDescription = uiOptions.enableMarkdownInDescription ? (_jsx(Markdown, { options: { disableParsingRawHTML: true }, children: description })) : (description); const help = uiOptions.help; const hidden = uiOptions.widget === 'hidden'; const classNames = ['form-group', 'field', `field-${getSchemaType(schema)}`]; if (!hideError && __errors && __errors.length > 0) { classNames.push('field-error has-error has-danger'); } if (uiSchema?.classNames) { if (process.env.NODE_ENV !== 'production') { console.warn("'uiSchema.classNames' is deprecated and may be removed in a major release; Use 'ui:classNames' instead."); } classNames.push(uiSchema.classNames); } if (uiOptions.classNames) { classNames.push(uiOptions.classNames); } const helpComponent = (_jsx(FieldHelpTemplate, { help: help, idSchema: idSchema, 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 || ((schema.anyOf || schema.oneOf) && !schemaUtils.isSelect(schema)) ? undefined : (_jsx(FieldErrorTemplate, { errors: __errors, errorSchema: errorSchema, idSchema: idSchema, schema: schema, uiSchema: uiSchema, registry: registry })); const fieldProps = { description: (_jsx(DescriptionFieldTemplate, { id: descriptionId(id), description: richDescription, schema: schema, uiSchema: uiSchema, registry: registry })), rawDescription: description, help: helpComponent, rawHelp: typeof help === 'string' ? help : undefined, errors: errorsComponent, rawErrors: hideError ? undefined : __errors, id, label, hidden, onChange, onKeyChange, onDropPropertyClick, required, disabled, readonly, hideError, displayLabel, classNames: classNames.join(' ').trim(), style: uiOptions.style, formContext, formData, schema, uiSchema, registry, }; const _AnyOfField = registry.fields.AnyOfField; const _OneOfField = registry.fields.OneOfField; const isReplacingAnyOrOneOf = uiSchema?.['ui:field'] && uiSchema?.['ui:fieldReplacesAnyOrOneOf'] === true; return (_jsx(FieldTemplate, { ...fieldProps, children: _jsxs(_Fragment, { children: [field, schema.anyOf && !isReplacingAnyOrOneOf && !schemaUtils.isSelect(schema) && (_jsx(_AnyOfField, { name: name, disabled: disabled, readonly: readonly, hideError: hideError, errorSchema: errorSchema, formData: formData, formContext: formContext, idPrefix: idPrefix, idSchema: idSchema, idSeparator: idSeparator, onBlur: props.onBlur, onChange: props.onChange, onFocus: props.onFocus, options: schema.anyOf.map((_schema) => schemaUtils.retrieveSchema(isObject(_schema) ? _schema : {}, formData)), registry: registry, required: required, schema: schema, uiSchema: uiSchema })), schema.oneOf && !isReplacingAnyOrOneOf && !schemaUtils.isSelect(schema) && (_jsx(_OneOfField, { name: name, disabled: disabled, readonly: readonly, hideError: hideError, errorSchema: errorSchema, formData: formData, formContext: formContext, idPrefix: idPrefix, idSchema: idSchema, idSeparator: idSeparator, onBlur: props.onBlur, onChange: props.onChange, onFocus: props.onFocus, options: schema.oneOf.map((_schema) => schemaUtils.retrieveSchema(isObject(_schema) ? _schema : {}, formData)), 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) { return !deepEquals(this.props, nextProps); } render() { return _jsx(SchemaFieldRender, { ...this.props }); } } export default SchemaField;