UNPKG

@rjsf/core

Version:

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

115 lines (114 loc) 8.15 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useState, useEffect } from 'react'; import { ANY_OF_KEY, CONST_KEY, DEFAULT_KEY, ERRORS_KEY, getDiscriminatorFieldFromSchema, hashObject, ID_KEY, ONE_OF_KEY, optionsList, PROPERTIES_KEY, getTemplate, getUiOptions, getWidget, } from '@rjsf/utils'; import get from 'lodash-es/get.js'; import has from 'lodash-es/has.js'; import isEmpty from 'lodash-es/isEmpty.js'; import noop from 'lodash-es/noop.js'; import omit from 'lodash-es/omit.js'; import set from 'lodash-es/set.js'; /** Gets the selected option from the list of `options`, using the `selectorField` to search inside each `option` for * the `properties[selectorField].default(or const)` that matches the given `value`. * * @param options - The list of schemas each representing a choice in the `oneOf` * @param selectorField - The name of the field that is common in all of the schemas that represents the selector field * @param value - The current value of the selector field from the data */ export function getSelectedOption(options, selectorField, value) { const defaultValue = '!@#!@$@#$!@$#'; const schemaOptions = options.map(({ schema }) => schema); return schemaOptions.find((option) => { const selector = get(option, [PROPERTIES_KEY, selectorField]); const result = get(selector, DEFAULT_KEY, get(selector, CONST_KEY, defaultValue)); return result === value; }); } /** Computes the `enumOptions` array from the schema and options. * * @param schema - The schema that contains the `options` * @param options - The options from the `schema` * @param schemaUtils - The SchemaUtilsType object used to call retrieveSchema, * @param [uiSchema] - The optional uiSchema for the schema * @param [formData] - The optional formData associated with the schema * @returns - The list of enumOptions for the `schema` and `options` * @throws - Error when no enum options were computed */ export function computeEnumOptions(schema, options, schemaUtils, uiSchema, formData) { const realOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); let tempSchema = schema; if (has(schema, ONE_OF_KEY)) { tempSchema = { ...schema, [ONE_OF_KEY]: realOptions }; } else if (has(schema, ANY_OF_KEY)) { tempSchema = { ...schema, [ANY_OF_KEY]: realOptions }; } const enumOptions = optionsList(tempSchema, uiSchema); if (!enumOptions) { throw new Error(`No enumOptions were computed from the schema ${JSON.stringify(tempSchema)}`); } return enumOptions; } /** The `LayoutMultiSchemaField` is an adaptation of the `MultiSchemaField` but changed considerably to only * support `anyOf`/`oneOf` fields that are being displayed in a `LayoutGridField` where the field selection is shown as * a radio group by default. It expects that a `selectorField` is provided (either directly via the `discriminator` * field or indirectly via `ui:optionsSchemaSelector` in the `uiSchema`) to help determine which `anyOf`/`oneOf` schema * is active. If no `selectorField` is specified, then an error is thrown. */ export default function LayoutMultiSchemaField(props) { const { name, baseType, disabled = false, formData, fieldPathId, onBlur, onChange, options, onFocus, registry, uiSchema, schema, autofocus, readonly, required, errorSchema, hideError = false, } = props; const { widgets, schemaUtils, globalUiOptions } = registry; const [enumOptions, setEnumOptions] = useState(computeEnumOptions(schema, options, schemaUtils, uiSchema, formData)); const id = get(fieldPathId, ID_KEY); const discriminator = getDiscriminatorFieldFromSchema(schema); const FieldErrorTemplate = getTemplate('FieldErrorTemplate', registry, options); const FieldTemplate = getTemplate('FieldTemplate', registry, options); const schemaHash = hashObject(schema); const optionsHash = hashObject(options); const uiSchemaHash = uiSchema ? hashObject(uiSchema) : ''; const formDataHash = formData ? hashObject(formData) : ''; useEffect(() => { setEnumOptions(computeEnumOptions(schema, options, schemaUtils, uiSchema, formData)); // We are using hashes in place of the dependencies // eslint-disable-next-line react-hooks/exhaustive-deps }, [schemaHash, optionsHash, schemaUtils, uiSchemaHash, formDataHash]); const { widget = discriminator ? 'radio' : 'select', title = '', placeholder = '', optionsSchemaSelector: selectorField = discriminator, hideError: uiSchemaHideError, ...uiOptions } = getUiOptions(uiSchema); if (!selectorField) { throw new Error('No selector field provided for the LayoutMultiSchemaField'); } const selectedOption = get(formData, selectorField); let optionSchema = get(enumOptions[0]?.schema, [PROPERTIES_KEY, selectorField], {}); const option = getSelectedOption(enumOptions, selectorField, selectedOption); // If the subschema doesn't declare a type, infer the type from the parent schema optionSchema = optionSchema?.type ? optionSchema : { ...optionSchema, type: option?.type || baseType }; const Widget = getWidget(optionSchema, widget, widgets); // The following code was copied from `@rjsf`'s `SchemaField` // Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children const hideFieldError = uiSchemaHideError === undefined ? hideError : Boolean(uiSchemaHideError); const rawErrors = get(errorSchema, [ERRORS_KEY], []); const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]); const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); /** Callback function that updates the selected option and adjusts the form data based on the structure of the new * option, calling the `onChange` callback with the adjusted formData. * * @param opt - If the option is undefined, we are going to clear the selection otherwise we * will use it as the index of the new option to select */ const onOptionChange = (opt) => { const newOption = getSelectedOption(enumOptions, selectorField, opt); const oldOption = getSelectedOption(enumOptions, selectorField, selectedOption); let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData); if (newFormData && newOption) { // Call getDefaultFormState to make sure defaults are populated on change. newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, 'excludeObjectChildren'); } if (newFormData) { set(newFormData, selectorField, opt); } // Pass the component name in the path onChange(newFormData, fieldPathId.path, undefined, id); }; // filtering the options based on the type of widget because `selectField` does not recognize the `convertOther` prop const widgetOptions = { enumOptions, ...uiOptions }; const errors = !hideFieldError && rawErrors.length > 0 ? (_jsx(FieldErrorTemplate, { fieldPathId: fieldPathId, schema: schema, errors: rawErrors, registry: registry })) : undefined; return (_jsx(FieldTemplate, { fieldPathId: fieldPathId, id: id, schema: schema, label: (title || schema.title) ?? '', disabled: disabled || (Array.isArray(enumOptions) && isEmpty(enumOptions)), uiSchema: uiSchema, required: required, readonly: !!readonly, registry: registry, displayLabel: displayLabel, errors: errors, onChange: onChange, onKeyRename: noop, onKeyRenameBlur: noop, onRemoveProperty: noop, children: _jsx(Widget, { id: id, name: name, schema: schema, label: (title || schema.title) ?? '', disabled: disabled || (Array.isArray(enumOptions) && isEmpty(enumOptions)), uiSchema: uiSchema, autofocus: autofocus, readonly: readonly, required: required, registry: registry, multiple: false, rawErrors: rawErrors, hideError: hideFieldError, hideLabel: !displayLabel, errorSchema: fieldErrorSchema, placeholder: placeholder, onChange: onOptionChange, onBlur: onBlur, onFocus: onFocus, value: selectedOption, options: widgetOptions, htmlName: fieldPathId.name }) })); }