@rjsf/core
Version:
A simple React component capable of building HTML forms out of a JSON schema.
115 lines (114 loc) • 8.15 kB
JavaScript
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 }) }));
}