@rjsf/core
Version:
A simple React component capable of building HTML forms out of a JSON schema.
664 lines (663 loc) • 33.5 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { useCallback, useMemo, useState } from 'react';
import { allowAdditionalItems, getTemplate, getUiOptions, getWidget, hashObject, isCustomWidget, isFixedItems, isFormDataAvailable, optionsList, shouldRenderOptionalField, toFieldPathId, useDeepCompareMemo, ITEMS_KEY, ID_KEY, TranslatableString, } from '@rjsf/utils';
import cloneDeep from 'lodash-es/cloneDeep.js';
import isObject from 'lodash-es/isObject.js';
import set from 'lodash-es/set.js';
import uniqueId from 'lodash-es/uniqueId.js';
/** Used to generate a unique ID for an element in a row */
function generateRowId() {
return uniqueId('rjsf-array-item-');
}
/** Converts the `formData` into `KeyedFormDataType` data, using the `generateRowId()` function to create the key
*
* @param formData - The data for the form
* @returns - The `formData` converted into a `KeyedFormDataType` element
*/
function generateKeyedFormData(formData) {
return !Array.isArray(formData)
? []
: formData.map((item) => {
return {
key: generateRowId(),
item,
};
});
}
/** Converts `KeyedFormDataType` data into the inner `formData`
*
* @param keyedFormData - The `KeyedFormDataType` to be converted
* @returns - The inner `formData` item(s) in the `keyedFormData`
*/
function keyedToPlainFormData(keyedFormData) {
if (Array.isArray(keyedFormData)) {
return keyedFormData.map((keyedItem) => keyedItem.item);
}
return [];
}
/** Determines whether the item described in the schema is always required, which is determined by whether any item
* may be null.
*
* @param itemSchema - The schema for the item
* @return - True if the item schema type does not contain the "null" type
*/
function isItemRequired(itemSchema) {
if (Array.isArray(itemSchema.type)) {
// While we don't yet support composite/nullable jsonschema types, it's
// future-proof to check for requirement against these.
return !itemSchema.type.includes('null');
}
// All non-null array item types are inherently required by design
return itemSchema.type !== 'null';
}
/** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding
* then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the
* `formData` matches that value, then false is returned, otherwise true is returned.
*
* @param registry - The registry
* @param schema - The schema for the field
* @param formItems - The list of items in the form
* @param [uiSchema] - The UiSchema for the field
* @returns - True if the item is addable otherwise false
*/
function canAddItem(registry, schema, formItems, uiSchema) {
let { addable } = getUiOptions(uiSchema, registry.globalUiOptions);
if (addable !== false) {
// if ui:options.addable was not explicitly set to false, we can add
// another item if we have not exceeded maxItems yet
if (schema.maxItems !== undefined) {
addable = formItems.length < schema.maxItems;
}
else {
addable = true;
}
}
return addable;
}
/** Helper method to compute item UI schema for both normal and fixed arrays
* Handles both static object and dynamic function cases
*
* @param uiSchema - The parent UI schema containing items definition
* @param item - The item data
* @param index - The index of the item
* @param formContext - The form context
* @returns The computed UI schema for the item
*/
function computeItemUiSchema(uiSchema, item, index, formContext) {
if (typeof uiSchema.items === 'function') {
try {
// Call the function with item data, index, and form context
// TypeScript now correctly infers the types thanks to the ArrayElement type in UiSchema
const result = uiSchema.items(item, index, formContext);
// Only use the result if it's truthy
return result;
}
catch (e) {
console.error(`Error executing dynamic uiSchema.items function for item at index ${index}:`, e);
// Fall back to undefined to allow the field to still render
return undefined;
}
}
else {
// Static object case - preserve undefined to maintain backward compatibility
return uiSchema.items;
}
}
/** Returns the default form information for an item based on the schema for that item. Deals with the possibility
* that the schema is fixed and allows additional items.
*/
function getNewFormDataRow(registry, schema) {
const { schemaUtils, globalFormOptions } = registry;
let itemSchema = schema.items;
if (globalFormOptions.useFallbackUiForUnsupportedType && !itemSchema) {
// If we don't have itemSchema and useFallbackUiForUnsupportedType is on, use an empty schema
itemSchema = {};
}
else if (isFixedItems(schema) && allowAdditionalItems(schema)) {
itemSchema = schema.additionalItems;
}
// Cast this as a T to work around schema utils being for T[] caused by the FieldProps<T[], S, F> call on the class
return schemaUtils.getDefaultFormState(itemSchema);
}
/** Renders an array as a set of checkboxes using the 'select' widget
*/
function ArrayAsMultiSelect(props) {
const { schema, fieldPathId, uiSchema, formData: items = [], disabled = false, readonly = false, autofocus = false, required = false, placeholder, onBlur, onFocus, registry, rawErrors, name, onSelectChange, } = props;
const { widgets, schemaUtils, globalFormOptions, globalUiOptions } = registry;
const itemsSchema = schemaUtils.retrieveSchema(schema.items, items);
const enumOptions = optionsList(itemsSchema, uiSchema);
const { widget = 'select', title: uiTitle, ...options } = getUiOptions(uiSchema, globalUiOptions);
const Widget = getWidget(schema, widget, widgets);
const label = uiTitle ?? schema.title ?? name;
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
// For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));
return (_jsx(Widget, { id: multiValueFieldPathId[ID_KEY], name: name, multiple: true, onChange: onSelectChange, onBlur: onBlur, onFocus: onFocus, options: { ...options, enumOptions }, schema: schema, uiSchema: uiSchema, registry: registry, value: items, disabled: disabled, readonly: readonly, required: required, label: label, hideLabel: !displayLabel, placeholder: placeholder, autofocus: autofocus, rawErrors: rawErrors, htmlName: multiValueFieldPathId.name }));
}
/** Renders an array using the custom widget provided by the user in the `uiSchema`
*/
function ArrayAsCustomWidget(props) {
const { schema, fieldPathId, uiSchema, disabled = false, readonly = false, autofocus = false, required = false, hideError, placeholder, onBlur, onFocus, formData: items = [], registry, rawErrors, name, onSelectChange, } = props;
const { widgets, schemaUtils, globalFormOptions, globalUiOptions } = registry;
const { widget, title: uiTitle, ...options } = getUiOptions(uiSchema, globalUiOptions);
const Widget = getWidget(schema, widget, widgets);
const label = uiTitle ?? schema.title ?? name;
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
// For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));
return (_jsx(Widget, { id: multiValueFieldPathId[ID_KEY], name: name, multiple: true, onChange: onSelectChange, onBlur: onBlur, onFocus: onFocus, options: options, schema: schema, uiSchema: uiSchema, registry: registry, value: items, disabled: disabled, readonly: readonly, hideError: hideError, required: required, label: label, hideLabel: !displayLabel, placeholder: placeholder, autofocus: autofocus, rawErrors: rawErrors, htmlName: multiValueFieldPathId.name }));
}
/** Renders an array of files using the `FileWidget`
*/
function ArrayAsFiles(props) {
const { schema, uiSchema, fieldPathId, name, disabled = false, readonly = false, autofocus = false, required = false, onBlur, onFocus, registry, formData: items = [], rawErrors, onSelectChange, } = props;
const { widgets, schemaUtils, globalFormOptions, globalUiOptions } = registry;
const { widget = 'files', title: uiTitle, ...options } = getUiOptions(uiSchema, globalUiOptions);
const Widget = getWidget(schema, widget, widgets);
const label = uiTitle ?? schema.title ?? name;
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
// For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));
return (_jsx(Widget, { options: options, id: multiValueFieldPathId[ID_KEY], name: name, multiple: true, onChange: onSelectChange, onBlur: onBlur, onFocus: onFocus, schema: schema, uiSchema: uiSchema, value: items, disabled: disabled, readonly: readonly, required: required, registry: registry, autofocus: autofocus, rawErrors: rawErrors, label: label, hideLabel: !displayLabel, htmlName: multiValueFieldPathId.name }));
}
/** Renders the individual array item using a `SchemaField` along with the additional properties that are needed to
* render the whole of the `ArrayFieldItemTemplate`.
*/
function ArrayFieldItem(props) {
const { itemKey, index, name, disabled, hideError, readonly, registry, uiOptions, parentUiSchema, canAdd, canRemove = true, canMoveUp, canMoveDown, itemSchema, itemData, itemUiSchema, itemFieldPathId, itemErrorSchema, autofocus, onBlur, onFocus, onChange, rawErrors, totalItems, title, handleAddItem, handleCopyItem, handleRemoveItem, handleReorderItems, } = props;
const { schemaUtils, fields: { ArraySchemaField, SchemaField }, globalUiOptions, } = registry;
const fieldPathId = useDeepCompareMemo(itemFieldPathId);
const ItemSchemaField = ArraySchemaField || SchemaField;
const ArrayFieldItemTemplate = getTemplate('ArrayFieldItemTemplate', registry, uiOptions);
const displayLabel = schemaUtils.getDisplayLabel(itemSchema, itemUiSchema, globalUiOptions);
const { description } = getUiOptions(itemUiSchema);
const hasDescription = !!description || !!itemSchema.description;
const { orderable = true, removable = true, copyable = false } = uiOptions;
const has = {
moveUp: orderable && canMoveUp,
moveDown: orderable && canMoveDown,
copy: copyable && canAdd,
remove: removable && canRemove,
toolbar: false,
};
has.toolbar = Object.keys(has).some((key) => has[key]);
const onAddItem = useCallback((event) => {
handleAddItem(event, index + 1);
}, [handleAddItem, index]);
const onCopyItem = useCallback((event) => {
handleCopyItem(event, index);
}, [handleCopyItem, index]);
const onRemoveItem = useCallback((event) => {
handleRemoveItem(event, index);
}, [handleRemoveItem, index]);
const onMoveUpItem = useCallback((event) => {
handleReorderItems(event, index, index - 1);
}, [handleReorderItems, index]);
const onMoveDownItem = useCallback((event) => {
handleReorderItems(event, index, index + 1);
}, [handleReorderItems, index]);
const templateProps = {
children: (_jsx(ItemSchemaField, { name: name, title: title, index: index, schema: itemSchema, uiSchema: itemUiSchema, formData: itemData, errorSchema: itemErrorSchema, fieldPathId: fieldPathId, required: isItemRequired(itemSchema), onChange: onChange, onBlur: onBlur, onFocus: onFocus, registry: registry, disabled: disabled, readonly: readonly, hideError: hideError, autofocus: autofocus, rawErrors: rawErrors })),
buttonsProps: {
fieldPathId,
disabled,
readonly,
canAdd,
hasCopy: has.copy,
hasMoveUp: has.moveUp,
hasMoveDown: has.moveDown,
hasRemove: has.remove,
index: index,
totalItems,
onAddItem,
onCopyItem,
onRemoveItem,
onMoveUpItem,
onMoveDownItem,
registry,
schema: itemSchema,
uiSchema: itemUiSchema,
},
itemKey,
className: 'rjsf-array-item',
disabled,
hasToolbar: has.toolbar,
index,
totalItems,
readonly,
registry,
schema: itemSchema,
uiSchema: itemUiSchema,
parentUiSchema,
displayLabel,
hasDescription,
};
return _jsx(ArrayFieldItemTemplate, { ...templateProps });
}
/** Renders a normal array without any limitations of length
*/
function NormalArray(props) {
const { schema, uiSchema = {}, errorSchema, fieldPathId, formData: formDataFromProps, name, title, disabled = false, readonly = false, autofocus = false, required = false, hideError = false, registry, onBlur, onFocus, rawErrors, onChange, keyedFormData, handleAddItem, handleCopyItem, handleRemoveItem, handleReorderItems, } = props;
const fieldTitle = schema.title || title || name;
const { schemaUtils, fields, formContext, globalFormOptions, globalUiOptions } = registry;
const { OptionalDataControlsField } = fields;
const uiOptions = getUiOptions(uiSchema, globalUiOptions);
const _schemaItems = isObject(schema.items) ? schema.items : {};
const itemsSchema = schemaUtils.retrieveSchema(_schemaItems);
const formData = keyedToPlainFormData(keyedFormData);
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(formDataFromProps);
const canAdd = canAddItem(registry, schema, formData, uiSchema) && (!renderOptionalField || hasFormData);
const actualFormData = hasFormData ? keyedFormData : [];
const extraClass = renderOptionalField ? ' rjsf-optional-array-field' : '';
// All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
const childFieldPathId = props.childFieldPathId ?? fieldPathId;
const optionalDataControl = renderOptionalField ? (_jsx(OptionalDataControlsField, { ...props, fieldPathId: childFieldPathId })) : undefined;
const arrayProps = {
canAdd,
items: actualFormData.map((keyedItem, index) => {
const { key, item } = keyedItem;
// While we are actually dealing with a single item of type T, the types require a T[], so cast
const itemCast = item;
const itemSchema = schemaUtils.retrieveSchema(_schemaItems, itemCast);
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemFieldPathId = toFieldPathId(index, globalFormOptions, childFieldPathId);
// Compute the item UI schema using the helper method
const itemUiSchema = computeItemUiSchema(uiSchema, item, index, formContext);
const itemProps = {
itemKey: key,
index,
name: name && `${name}-${index}`,
registry,
uiOptions,
hideError,
readonly,
disabled,
required,
title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined,
canAdd,
canMoveUp: index > 0,
canMoveDown: index < formData.length - 1,
itemSchema,
itemFieldPathId,
itemErrorSchema,
itemData: itemCast,
itemUiSchema,
autofocus: autofocus && index === 0,
onBlur,
onFocus,
rawErrors,
totalItems: keyedFormData.length,
handleAddItem,
handleCopyItem,
handleRemoveItem,
handleReorderItems,
onChange,
};
return _jsx(ArrayFieldItem, { ...itemProps }, key);
}),
className: `rjsf-field rjsf-field-array rjsf-field-array-of-${itemsSchema.type}${extraClass}`,
disabled,
fieldPathId,
uiSchema,
onAddClick: handleAddItem,
readonly,
required,
schema,
title: fieldTitle,
formData,
rawErrors,
registry,
optionalDataControl,
};
const Template = getTemplate('ArrayFieldTemplate', registry, uiOptions);
return _jsx(Template, { ...arrayProps });
}
/** Renders an array that has a maximum limit of items
*/
function FixedArray(props) {
const { schema, uiSchema = {}, formData, errorSchema, fieldPathId, name, title, disabled = false, readonly = false, autofocus = false, required = false, hideError = false, registry, onBlur, onFocus, rawErrors, keyedFormData, onChange, handleAddItem, handleCopyItem, handleRemoveItem, handleReorderItems, } = props;
let { formData: items = [] } = props;
const fieldTitle = schema.title || title || name;
const { schemaUtils, fields, formContext, globalFormOptions, globalUiOptions } = registry;
const uiOptions = getUiOptions(uiSchema, globalUiOptions);
const { OptionalDataControlsField } = fields;
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(formData);
const _schemaItems = isObject(schema.items) ? schema.items : [];
const itemSchemas = _schemaItems.map((item, index) => schemaUtils.retrieveSchema(item, items[index]));
const additionalSchema = isObject(schema.additionalItems)
? schemaUtils.retrieveSchema(schema.additionalItems, formData)
: null;
// All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
const childFieldPathId = props.childFieldPathId ?? fieldPathId;
if (items.length < itemSchemas.length) {
// to make sure at least all fixed items are generated
items = items.concat(new Array(itemSchemas.length - items.length));
}
const actualFormData = hasFormData ? keyedFormData : [];
const extraClass = renderOptionalField ? ' rjsf-optional-array-field' : '';
const optionalDataControl = renderOptionalField ? (_jsx(OptionalDataControlsField, { ...props, fieldPathId: childFieldPathId })) : undefined;
// These are the props passed into the render function
const canAdd = canAddItem(registry, schema, items, uiSchema) &&
!!additionalSchema &&
(!renderOptionalField || hasFormData);
const arrayProps = {
canAdd,
className: `rjsf-field rjsf-field-array rjsf-field-array-fixed-items${extraClass}`,
disabled,
fieldPathId,
formData,
items: actualFormData.map((keyedItem, index) => {
const { key, item } = keyedItem;
// While we are actually dealing with a single item of type T, the types require a T[], so cast
const itemCast = item;
const additional = index >= itemSchemas.length;
const itemSchema = (additional && isObject(schema.additionalItems)
? schemaUtils.retrieveSchema(schema.additionalItems, itemCast)
: itemSchemas[index]) || {};
const itemFieldPathId = toFieldPathId(index, globalFormOptions, childFieldPathId);
// Compute the item UI schema - handle both static and dynamic cases
let itemUiSchema;
if (additional) {
// For additional items, use additionalItems uiSchema
itemUiSchema = uiSchema.additionalItems;
}
else {
// For fixed items, uiSchema.items can be an array, a function, or a single object
if (Array.isArray(uiSchema.items)) {
itemUiSchema = uiSchema.items[index];
}
else {
// Use the helper method for function or static object cases
itemUiSchema = computeItemUiSchema(uiSchema, item, index, formContext);
}
}
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemProps = {
index,
itemKey: key,
name: name && `${name}-${index}`,
registry,
uiOptions,
hideError,
readonly,
disabled,
required,
title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined,
canAdd,
canRemove: additional,
canMoveUp: index >= itemSchemas.length + 1,
canMoveDown: additional && index < items.length - 1,
itemSchema,
itemData: itemCast,
itemUiSchema,
itemFieldPathId,
itemErrorSchema,
autofocus: autofocus && index === 0,
onBlur,
onFocus,
rawErrors,
totalItems: keyedFormData.length,
onChange,
handleAddItem,
handleCopyItem,
handleRemoveItem,
handleReorderItems,
};
return _jsx(ArrayFieldItem, { ...itemProps }, key);
}),
onAddClick: handleAddItem,
readonly,
required,
registry,
schema,
uiSchema,
title: fieldTitle,
errorSchema,
rawErrors,
optionalDataControl,
};
const Template = getTemplate('ArrayFieldTemplate', registry, uiOptions);
return _jsx(Template, { ...arrayProps });
}
/** A custom hook that handles the updating of the keyedFormData from an external `formData` change as well as
* internally by the `ArrayField`. If there was an external `formData` change, then the `keyedFormData` is recomputed
* in order to preserve the unique keys from the old `keyedFormData` to the new `formData`. Along with the
* `keyedFormData` this hook also returns an `updateKeyedFormData()` function for use by the `ArrayField`. The detection
* of external `formData` are handled by storing the hash of that `formData` along with the `keyedFormData` associated
* with it. The `updateKeyedFormData()` will update that hash whenever the `keyedFormData` is modified and as well as
* returning the plain `formData` from the `keyedFormData`.
*/
function useKeyedFormData(formData = []) {
const newHash = useMemo(() => hashObject(formData), [formData]);
const [state, setState] = useState(() => ({
formDataHash: newHash,
keyedFormData: generateKeyedFormData(formData),
}));
let { keyedFormData, formDataHash } = state;
if (newHash !== formDataHash) {
const nextFormData = Array.isArray(formData) ? formData : [];
const previousKeyedFormData = keyedFormData || [];
keyedFormData =
nextFormData.length === previousKeyedFormData.length
? previousKeyedFormData.map((previousKeyedFormDatum, index) => ({
key: previousKeyedFormDatum.key,
item: nextFormData[index],
}))
: generateKeyedFormData(nextFormData);
formDataHash = newHash;
setState({ formDataHash, keyedFormData });
}
const updateKeyedFormData = useCallback((newData) => {
const plainFormData = keyedToPlainFormData(newData);
const newHash = hashObject(plainFormData);
setState({ formDataHash: newHash, keyedFormData: newData });
return plainFormData;
}, []);
return { keyedFormData, updateKeyedFormData };
}
/** The `ArrayField` component is used to render a field in the schema that is of type `array`. It supports both normal
* and fixed array, allowing user to add and remove elements from the array data.
*/
export default function ArrayField(props) {
const { schema, uiSchema, errorSchema, fieldPathId, registry, formData, onChange } = props;
const { globalFormOptions, schemaUtils, translateString } = registry;
const { keyedFormData, updateKeyedFormData } = useKeyedFormData(formData);
// All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
const childFieldPathId = props.childFieldPathId ?? fieldPathId;
/** Callback handler for when the user clicks on the add or add at index buttons. Creates a new row of keyed form data
* either at the end of the list (when index is not specified) or inserted at the `index` when it is, adding it into
* the state, and then returning `onChange()` with the plain form data converted from the keyed data
*
* @param event - The event for the click
* @param [index] - The optional index at which to add the new data
*/
const handleAddItem = useCallback((event, index) => {
if (event) {
event.preventDefault();
}
let newErrorSchema;
if (errorSchema) {
newErrorSchema = {};
for (const idx in errorSchema) {
const i = parseInt(idx);
if (index === undefined || i < index) {
set(newErrorSchema, [i], errorSchema[idx]);
}
else if (i >= index) {
set(newErrorSchema, [i + 1], errorSchema[idx]);
}
}
}
const newKeyedFormDataRow = {
key: generateRowId(),
item: getNewFormDataRow(registry, schema),
};
const newKeyedFormData = [...keyedFormData];
if (index !== undefined) {
newKeyedFormData.splice(index, 0, newKeyedFormDataRow);
}
else {
newKeyedFormData.push(newKeyedFormDataRow);
}
onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema);
}, [keyedFormData, registry, schema, onChange, updateKeyedFormData, errorSchema, childFieldPathId]);
/** Callback handler for when the user clicks on the copy button on an existing array element. Clones the row of
* keyed form data at the `index` into the next position in the state, and then returning `onChange()` with the plain
* form data converted from the keyed data
*
* @param index - The index at which the copy button is clicked
*/
const handleCopyItem = useCallback((event, index) => {
if (event) {
event.preventDefault();
}
let newErrorSchema;
if (errorSchema) {
newErrorSchema = {};
for (const idx in errorSchema) {
const i = parseInt(idx);
if (i <= index) {
set(newErrorSchema, [i], errorSchema[idx]);
}
else if (i > index) {
set(newErrorSchema, [i + 1], errorSchema[idx]);
}
}
}
const newKeyedFormDataRow = {
key: generateRowId(),
item: cloneDeep(keyedFormData[index].item),
};
const newKeyedFormData = [...keyedFormData];
if (index !== undefined) {
newKeyedFormData.splice(index + 1, 0, newKeyedFormDataRow);
}
else {
newKeyedFormData.push(newKeyedFormDataRow);
}
onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema);
}, [keyedFormData, onChange, updateKeyedFormData, errorSchema, childFieldPathId]);
/** Callback handler for when the user clicks on the remove button on an existing array element. Removes the row of
* keyed form data at the `index` in the state, and then returning `onChange()` with the plain form data converted
* from the keyed data
*
* @param index - The index at which the remove button is clicked
*/
const handleRemoveItem = useCallback((event, index) => {
if (event) {
event.preventDefault();
}
// refs #195: revalidate to ensure properly reindexing errors
let newErrorSchema;
if (errorSchema) {
newErrorSchema = {};
for (const idx in errorSchema) {
const i = parseInt(idx);
if (i < index) {
set(newErrorSchema, [i], errorSchema[idx]);
}
else if (i > index) {
set(newErrorSchema, [i - 1], errorSchema[idx]);
}
}
}
const newKeyedFormData = keyedFormData.filter((_, i) => i !== index);
onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema);
}, [keyedFormData, onChange, updateKeyedFormData, errorSchema, childFieldPathId]);
/** Callback handler for when the user clicks on one of the move item buttons on an existing array element. Moves the
* row of keyed form data at the `index` to the `newIndex` in the state, and then returning `onChange()` with the
* plain form data converted from the keyed data
*
* @param index - The index of the item to move
* @param newIndex - The index to where the item is to be moved
*/
const handleReorderItems = useCallback((event, index, newIndex) => {
if (event) {
event.preventDefault();
event.currentTarget.blur();
}
let newErrorSchema;
if (errorSchema) {
newErrorSchema = {};
for (const idx in errorSchema) {
const i = parseInt(idx);
if (i == index) {
set(newErrorSchema, [newIndex], errorSchema[index]);
}
else if (i == newIndex) {
set(newErrorSchema, [index], errorSchema[newIndex]);
}
else {
set(newErrorSchema, [idx], errorSchema[i]);
}
}
}
function reOrderArray() {
// Copy item
const _newKeyedFormData = keyedFormData.slice();
// Moves item from index to newIndex
_newKeyedFormData.splice(index, 1);
_newKeyedFormData.splice(newIndex, 0, keyedFormData[index]);
return _newKeyedFormData;
}
const newKeyedFormData = reOrderArray();
onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema);
}, [keyedFormData, onChange, updateKeyedFormData, errorSchema, childFieldPathId]);
/** Callback handler used to deal with changing the value of the data in the array at the `index`. Calls the
* `onChange` callback with the updated form data
*
* @param index - The index of the item being changed
*/
const handleChange = useCallback((value, path, newErrorSchema, id) => {
onChange(
// We need to treat undefined items as nulls to have validation.
// See https://github.com/tdegrunt/jsonschema/issues/206
value === undefined ? null : value, path, newErrorSchema, id);
}, [onChange]);
/** Callback handler used to change the value for a checkbox */
const onSelectChange = useCallback((value) => {
onChange(value, childFieldPathId.path, undefined, childFieldPathId?.[ID_KEY]);
}, [onChange, childFieldPathId]);
const arrayAsMultiProps = {
...props,
formData,
fieldPathId: childFieldPathId,
onSelectChange: onSelectChange,
};
const arrayProps = {
...props,
handleAddItem,
handleCopyItem,
handleRemoveItem,
handleReorderItems,
keyedFormData,
onChange: handleChange,
};
if (!(ITEMS_KEY in schema)) {
if (!globalFormOptions.useFallbackUiForUnsupportedType) {
const uiOptions = getUiOptions(uiSchema);
const UnsupportedFieldTemplate = getTemplate('UnsupportedFieldTemplate', registry, uiOptions);
return (_jsx(UnsupportedFieldTemplate, { schema: schema, fieldPathId: fieldPathId, reason: translateString(TranslatableString.MissingItems), registry: registry }));
}
// Add an items schema with type as undefined so it triggers FallbackField later on
const fallbackSchema = { ...schema, [ITEMS_KEY]: { type: undefined } };
arrayAsMultiProps.schema = fallbackSchema;
arrayProps.schema = fallbackSchema;
}
if (schemaUtils.isMultiSelect(arrayAsMultiProps.schema)) {
// If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
return _jsx(ArrayAsMultiSelect, { ...arrayAsMultiProps });
}
if (isCustomWidget(uiSchema)) {
return _jsx(ArrayAsCustomWidget, { ...arrayAsMultiProps });
}
if (isFixedItems(arrayAsMultiProps.schema)) {
return _jsx(FixedArray, { ...arrayProps });
}
if (schemaUtils.isFilesArray(arrayAsMultiProps.schema, uiSchema)) {
return _jsx(ArrayAsFiles, { ...arrayAsMultiProps });
}
return _jsx(NormalArray, { ...arrayProps });
}