UNPKG

@rjsf/core

Version:

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

155 lines (154 loc) 9.11 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Component } from 'react'; import get from 'lodash-es/get.js'; import isEmpty from 'lodash-es/isEmpty.js'; import omit from 'lodash-es/omit.js'; import { ANY_OF_KEY, deepEquals, ERRORS_KEY, getDiscriminatorFieldFromSchema, getUiOptions, getWidget, mergeSchemas, ONE_OF_KEY, TranslatableString, } from '@rjsf/utils'; /** The `AnyOfField` component is used to render a field in the schema that is an `anyOf`, `allOf` or `oneOf`. It tracks * the currently selected option and cleans up any irrelevant data in `formData`. * * @param props - The `FieldProps` for this template */ class AnyOfField extends Component { /** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state * * @param props - The `FieldProps` for this template */ constructor(props) { super(props); const { formData, options, registry: { schemaUtils }, } = this.props; // cache the retrieved options in state in case they have $refs to save doing it later const retrievedOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); this.state = { retrievedOptions, selectedOption: this.getMatchingOption(0, formData, retrievedOptions), }; } /** React lifecycle method that is called when the props and/or state for this component is updated. It recomputes the * currently selected option based on the overall `formData` * * @param prevProps - The previous `FieldProps` for this template * @param prevState - The previous `AnyOfFieldState` for this template */ componentDidUpdate(prevProps, prevState) { const { formData, options, idSchema } = this.props; const { selectedOption } = this.state; let newState = this.state; if (!deepEquals(prevProps.options, options)) { const { registry: { schemaUtils }, } = this.props; // re-cache the retrieved options in state in case they have $refs to save doing it later const retrievedOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); newState = { selectedOption, retrievedOptions }; } if (!deepEquals(formData, prevProps.formData) && idSchema.$id === prevProps.idSchema.$id) { const { retrievedOptions } = newState; const matchingOption = this.getMatchingOption(selectedOption, formData, retrievedOptions); if (prevState && matchingOption !== selectedOption) { newState = { selectedOption: matchingOption, retrievedOptions }; } } if (newState !== this.state) { this.setState(newState); } } /** Determines the best matching option for the given `formData` and `options`. * * @param formData - The new formData * @param options - The list of options to choose from * @return - The index of the `option` that best matches the `formData` */ getMatchingOption(selectedOption, formData, options) { const { schema, registry: { schemaUtils }, } = this.props; const discriminator = getDiscriminatorFieldFromSchema(schema); const option = schemaUtils.getClosestMatchingOption(formData, options, selectedOption, discriminator); return option; } /** Callback handler to remember what the currently selected option is. In addition to that the `formData` is updated * to remove properties that are not part of the newly selected option schema, and then the updated data is passed to * the `onChange` handler. * * @param option - The new option value being selected */ onOptionChange = (option) => { const { selectedOption, retrievedOptions } = this.state; const { formData, onChange, registry } = this.props; const { schemaUtils } = registry; const intOption = option !== undefined ? parseInt(option, 10) : -1; if (intOption === selectedOption) { return; } const newOption = intOption >= 0 ? retrievedOptions[intOption] : undefined; const oldOption = selectedOption >= 0 ? retrievedOptions[selectedOption] : undefined; let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData); if (newOption) { // Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren" // so that only the root objects themselves are created without adding undefined children properties newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, 'excludeObjectChildren'); } this.setState({ selectedOption: intOption }, () => { onChange(newFormData, undefined, this.getFieldId()); }); }; getFieldId() { const { idSchema, schema } = this.props; return `${idSchema.$id}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`; } /** Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData` */ render() { const { name, disabled = false, errorSchema = {}, formContext, onBlur, onFocus, readonly, registry, schema, uiSchema, } = this.props; const { widgets, fields, translateString, globalUiOptions, schemaUtils } = registry; const { SchemaField: _SchemaField } = fields; const { selectedOption, retrievedOptions } = this.state; const { widget = 'select', placeholder, autofocus, autocomplete, title = schema.title, ...uiOptions } = getUiOptions(uiSchema, globalUiOptions); const Widget = getWidget({ type: 'number' }, widget, widgets); const rawErrors = get(errorSchema, ERRORS_KEY, []); const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]); const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null; let optionSchema; if (option) { // merge top level required field const { required } = schema; // Merge in all the non-oneOf/anyOf properties and also skip the special ADDITIONAL_PROPERTY_FLAG property optionSchema = required ? mergeSchemas({ required }, option) : option; } // First we will check to see if there is an anyOf/oneOf override for the UI schema let optionsUiSchema = []; if (ONE_OF_KEY in schema && uiSchema && ONE_OF_KEY in uiSchema) { if (Array.isArray(uiSchema[ONE_OF_KEY])) { optionsUiSchema = uiSchema[ONE_OF_KEY]; } else { console.warn(`uiSchema.oneOf is not an array for "${title || name}"`); } } else if (ANY_OF_KEY in schema && uiSchema && ANY_OF_KEY in uiSchema) { if (Array.isArray(uiSchema[ANY_OF_KEY])) { optionsUiSchema = uiSchema[ANY_OF_KEY]; } else { console.warn(`uiSchema.anyOf is not an array for "${title || name}"`); } } // Then we pick the one that matches the selected option index, if one exists otherwise default to the main uiSchema let optionUiSchema = uiSchema; if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) { optionUiSchema = optionsUiSchema[selectedOption]; } const translateEnum = title ? TranslatableString.TitleOptionPrefix : TranslatableString.OptionPrefix; const translateParams = title ? [title] : []; const enumOptions = retrievedOptions.map((opt, index) => { // Also see if there is an override title in the uiSchema for each option, otherwise use the title from the option const { title: uiTitle = opt.title } = getUiOptions(optionsUiSchema[index]); return { label: uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))), value: index, }; }); return (_jsxs("div", { className: 'panel panel-default panel-body', children: [_jsx("div", { className: 'form-group', children: _jsx(Widget, { id: this.getFieldId(), name: `${name}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`, schema: { type: 'number', default: 0 }, onChange: this.onOptionChange, onBlur: onBlur, onFocus: onFocus, disabled: disabled || isEmpty(enumOptions), multiple: false, rawErrors: rawErrors, errorSchema: fieldErrorSchema, value: selectedOption >= 0 ? selectedOption : undefined, options: { enumOptions, ...uiOptions }, registry: registry, formContext: formContext, placeholder: placeholder, autocomplete: autocomplete, autofocus: autofocus, label: title ?? name, hideLabel: !displayLabel, readonly: readonly }) }), optionSchema && optionSchema.type !== 'null' && (_jsx(_SchemaField, { ...this.props, schema: optionSchema, uiSchema: optionUiSchema }))] })); } } export default AnyOfField;