@rjsf/core
Version:
A simple React component capable of building HTML forms out of a JSON schema.
237 lines (236 loc) • 13.3 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useState } from 'react';
import { ADDITIONAL_PROPERTY_FLAG, ANY_OF_KEY, getTemplate, getUiOptions, isFormDataAvailable, orderProperties, shouldRenderOptionalField, toFieldPathId, useDeepCompareMemo, ONE_OF_KEY, PROPERTIES_KEY, REF_KEY, TranslatableString, } from '@rjsf/utils';
import Markdown from 'markdown-to-jsx';
import get from 'lodash-es/get.js';
import has from 'lodash-es/has.js';
import isObject from 'lodash-es/isObject.js';
import set from 'lodash-es/set.js';
import { ADDITIONAL_PROPERTY_KEY_REMOVE } from '../constants.js';
/** Returns a flag indicating whether the `name` field is required in the object schema
*
* @param schema - The schema to check
* @param name - The name of the field to check for required-ness
* @returns - True if the field `name` is required, false otherwise
*/
function isRequired(schema, name) {
return Array.isArray(schema.required) && schema.required.indexOf(name) !== -1;
}
/** Returns a default value to be used for a new additional schema property of the given `type`
*
* @param translateString - The string translation function from the registry
* @param type - The type of the new additional schema property
*/
function getDefaultValue(translateString, type) {
switch (type) {
case 'array':
return [];
case 'boolean':
return false;
case 'null':
return null;
case 'number':
return 0;
case 'object':
return {};
case 'string':
default:
// We don't have a datatype for some reason (perhaps additionalProperties was true)
return translateString(TranslatableString.NewStringDefault);
}
}
/** The `ObjectFieldProperty` component is used to render the `SchemaField` for a child property of an object
*/
function ObjectFieldProperty(props) {
const { fieldPathId, schema, registry, uiSchema, errorSchema, formData, onChange, onBlur, onFocus, disabled, readonly, required, hideError, propertyName, handleKeyRename, handleRemoveProperty, addedByAdditionalProperties, } = props;
const [wasPropertyKeyModified, setWasPropertyKeyModified] = useState(false);
const { globalFormOptions, fields } = registry;
const { SchemaField } = fields;
const innerFieldIdPathId = useDeepCompareMemo(toFieldPathId(propertyName, globalFormOptions, fieldPathId.path));
/** Returns the `onPropertyChange` handler for the `name` field. Handles the special case where a user is attempting
* to clear the data for a field added as an additional property. Calls the `onChange()` handler with the updated
* formData.
*
* @param name - The name of the property
* @param addedByAdditionalProperties - Flag indicating whether this property is an additional property
* @returns - The onPropertyChange callback for the `name` property
*/
const onPropertyChange = useCallback((value, path, newErrorSchema, id) => {
if (value === undefined && addedByAdditionalProperties) {
// Don't set value = undefined for fields added by additionalProperties. Doing so removes them from the
// formData, which causes them to completely disappear (including the input field for the property name). Unlike
// fields which are "mandated" by the schema, these fields can be set to undefined by clicking a "delete field"
// button, so set empty values to the empty string.
value = '';
}
onChange(value, path, newErrorSchema, id);
}, [onChange, addedByAdditionalProperties]);
/** The key change event handler; Called when the key associated with a field is changed for an additionalProperty.
* simply returns a function that call the `handleKeyChange()` event with the value
*/
const onKeyRename = useCallback((value) => {
if (propertyName !== value) {
setWasPropertyKeyModified(true);
}
handleKeyRename(propertyName, value);
}, [propertyName, handleKeyRename]);
/** Returns a callback the handle the blur event, getting the value from the target and passing that along to the
* `handleKeyChange` function
*/
const onKeyRenameBlur = useCallback((event) => {
const { target: { value }, } = event;
onKeyRename(value);
}, [onKeyRename]);
/** The property drop/removal event handler; Called when a field is removed in an additionalProperty context
*/
const onRemoveProperty = useCallback(() => {
handleRemoveProperty(propertyName);
}, [propertyName, handleRemoveProperty]);
return (_jsx(SchemaField, { name: propertyName, required: required, schema: schema, uiSchema: uiSchema, errorSchema: errorSchema, fieldPathId: innerFieldIdPathId, formData: formData, wasPropertyKeyModified: wasPropertyKeyModified, onKeyRename: onKeyRename, onKeyRenameBlur: onKeyRenameBlur, onRemoveProperty: onRemoveProperty, onChange: onPropertyChange, onBlur: onBlur, onFocus: onFocus, registry: registry, disabled: disabled, readonly: readonly, hideError: hideError }));
}
/** The `ObjectField` component is used to render a field in the schema that is of type `object`. It tracks whether an
* additional property key was modified and what it was modified to
*
* @param props - The `FieldProps` for this template
*/
export default function ObjectField(props) {
const { schema: rawSchema, uiSchema = {}, formData, errorSchema, fieldPathId, name, required = false, disabled, readonly, hideError, onBlur, onFocus, onChange, registry, title, } = props;
const { fields, schemaUtils, translateString, globalUiOptions } = registry;
const { OptionalDataControlsField } = fields;
const schema = schemaUtils.retrieveSchema(rawSchema, formData, true);
const uiOptions = getUiOptions(uiSchema, globalUiOptions);
const { properties: schemaProperties = {} } = schema;
// All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
const childFieldPathId = props.childFieldPathId ?? fieldPathId;
const templateTitle = uiOptions.title ?? schema.title ?? title ?? name;
const description = uiOptions.description ?? schema.description;
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(formData);
let orderedProperties = [];
/** Computes the next available key name from the `preferredKey`, indexing through the already existing keys until one
* that is already not assigned is found.
*
* @param preferredKey - The preferred name of a new key
* @param [formData] - The form data in which to check if the desired key already exists
* @returns - The name of the next available key from `preferredKey`
*/
const getAvailableKey = useCallback((preferredKey, formData) => {
const { duplicateKeySuffixSeparator = '-' } = getUiOptions(uiSchema, globalUiOptions);
let index = 0;
let newKey = preferredKey;
while (has(formData, newKey)) {
newKey = `${preferredKey}${duplicateKeySuffixSeparator}${++index}`;
}
return newKey;
}, [uiSchema, globalUiOptions]);
/** Handles the adding of a new additional property on the given `schema`. Calls the `onChange` callback once the new
* default data for that field has been added to the formData.
*/
const onAddProperty = useCallback(() => {
if (!(schema.additionalProperties || schema.patternProperties)) {
return;
}
const { translateString } = registry;
const newFormData = { ...formData };
const newKey = getAvailableKey('newKey', newFormData);
if (schema.patternProperties) {
// Cast this to make the `set` work properly
set(newFormData, newKey, null);
}
else {
let type = undefined;
let constValue = undefined;
let defaultValue = undefined;
if (isObject(schema.additionalProperties)) {
type = schema.additionalProperties.type;
constValue = schema.additionalProperties.const;
defaultValue = schema.additionalProperties.default;
let apSchema = schema.additionalProperties;
if (REF_KEY in apSchema) {
const { schemaUtils } = registry;
apSchema = schemaUtils.retrieveSchema({ [REF_KEY]: apSchema[REF_KEY] }, formData);
type = apSchema.type;
constValue = apSchema.const;
defaultValue = apSchema.default;
}
if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) {
type = 'object';
}
}
const newValue = constValue ?? defaultValue ?? getDefaultValue(translateString, type);
// Cast this to make the `set` work properly
set(newFormData, newKey, newValue);
}
onChange(newFormData, childFieldPathId.path);
}, [formData, onChange, registry, childFieldPathId, getAvailableKey, schema]);
/** Returns a callback function that deals with the rename of a key for an additional property for a schema. That
* callback will attempt to rename the key and move the existing data to that key, calling `onChange` when it does.
*
* @param oldKey - The old key for the field
* @param newKey - The new key for the field
* @returns - The key change callback function
*/
const handleKeyRename = useCallback((oldKey, newKey) => {
if (oldKey !== newKey) {
const actualNewKey = getAvailableKey(newKey, formData);
const newFormData = {
...formData,
};
const newKeys = { [oldKey]: actualNewKey };
const keyValues = Object.keys(newFormData).map((key) => {
const newKey = newKeys[key] || key;
return { [newKey]: newFormData[key] };
});
const renamedObj = Object.assign({}, ...keyValues);
onChange(renamedObj, childFieldPathId.path);
}
}, [formData, onChange, childFieldPathId, getAvailableKey]);
/** Handles the remove click which calls the `onChange` callback with the special ADDITIONAL_PROPERTY_FIELD_REMOVE
* value for the path plus the key to be removed
*/
const handleRemoveProperty = useCallback((key) => {
onChange(ADDITIONAL_PROPERTY_KEY_REMOVE, [...childFieldPathId.path, key]);
}, [onChange, childFieldPathId]);
if (!renderOptionalField || hasFormData) {
try {
const properties = Object.keys(schemaProperties);
orderedProperties = orderProperties(properties, uiOptions.order);
}
catch (err) {
return (_jsxs("div", { children: [_jsx("p", { className: 'rjsf-config-error', style: { color: 'red' }, children: _jsx(Markdown, { options: { disableParsingRawHTML: true }, children: translateString(TranslatableString.InvalidObjectField, [name || 'root', err.message]) }) }), _jsx("pre", { children: JSON.stringify(schema) })] }));
}
}
const Template = getTemplate('ObjectFieldTemplate', registry, uiOptions);
const optionalDataControl = renderOptionalField ? (_jsx(OptionalDataControlsField, { ...props, fieldPathId: childFieldPathId, schema: schema })) : undefined;
const templateProps = {
// getDisplayLabel() always returns false for object types, so just check the `uiOptions.label`
title: uiOptions.label === false ? '' : templateTitle,
description: uiOptions.label === false ? undefined : description,
properties: orderedProperties.map((name) => {
const addedByAdditionalProperties = has(schema, [PROPERTIES_KEY, name, ADDITIONAL_PROPERTY_FLAG]);
const fieldUiSchema = addedByAdditionalProperties ? uiSchema.additionalProperties : uiSchema[name];
const hidden = getUiOptions(fieldUiSchema).widget === 'hidden';
const content = (_jsx(ObjectFieldProperty, { propertyName: name, required: isRequired(schema, name), schema: get(schema, [PROPERTIES_KEY, name], {}), uiSchema: fieldUiSchema, errorSchema: get(errorSchema, [name]), fieldPathId: childFieldPathId, formData: get(formData, [name]), handleKeyRename: handleKeyRename, handleRemoveProperty: handleRemoveProperty, addedByAdditionalProperties: addedByAdditionalProperties, onChange: onChange, onBlur: onBlur, onFocus: onFocus, registry: registry, disabled: disabled, readonly: readonly, hideError: hideError }, name));
return {
content,
name,
readonly,
disabled,
required,
hidden,
};
}),
readonly,
disabled,
required,
fieldPathId,
uiSchema,
errorSchema,
schema,
formData,
registry,
optionalDataControl,
className: renderOptionalField ? 'rjsf-optional-object-field' : undefined,
};
return _jsx(Template, { ...templateProps, onAddProperty: onAddProperty });
}