diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bbc638518..6899a3a576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,13 +20,15 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/antd - Updated most of the widgets to get `formContext` from the `registry` instead of the `props` since it will no longer be passed -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/chakra-ui -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/core @@ -34,62 +36,83 @@ should change the heading of the (upcoming) version to include a major version b - Updated `ArrayField`, `BooleanField`, `LayoutMultiSchemaField`, `MultiSchemaField`, `ObjectField`, `SchemaField`, `StringField` and `BaseInputTemplate` to remove `formContext` from the props - Updated `ObjectField` to refactor the code from a class component to two stateless functional components, replacing the 3 generator-props with the 4 memoized props mentioned in the `@rjsf/utils` changes - Updated `Form` to "memoize" the `fieldPathId` and `registry` into the `FormState`, adding a `toIChangeEvent()` helper to restrict the state returned on the `IChangeEvent` interface callbacks -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Added `nameGenerator` prop to `Form` component to enable custom HTML `name` attribute generation for form fields +- Updated `ArrayField` to refactor code from a class component to seven stateless functional components, replacing the 4 generator-props with the 5 memoized props mentioned in the `@rjsf/utils` changes + - BREAKING CHANGE: The refactor included rendering the `ArrayFieldItemTemplate` directly by the `ArrayField` rather than deferring it to the `ArrayFieldTemplate` + - Updated the `ArrayField` tests to adjust for the rendering change AND to remove 2 tests due to them no longer being valid +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes +- BREAKING CHANGE: Updated `ArrayFieldItemButtonsTemplate` to replace the old callback-generator functions with the new memoizable callback functions +- Fixed a bug in `Form` to avoid getting errors being reported at the root level via `onChange` when there aren't ## @rjsf/daisyui - Updated the test mocks to remove `formContext` for the widget mock -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes +- BREAKING CHANGE: Updated `ArrayFieldItemButtonsTemplate` to replace the old callback-generator functions with the new memoizable callback functions ## @rjsf/fluentui-rc -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/mantine -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/mui - Updated `BaseInputTemplate` and `SelectWidget` to remove `formContext` from the props -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/primereact - Updated `SelectWidget` to remove `formContext` from the props -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/react-bootstrap -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/semantic-ui -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `RadioWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes +- Updated `ArrayFieldItemTemplate` to refactor the getting of the `semanticProps` from `ArrayFieldTemplate`, using the new `parentUiSchema` prop to maintain the feature ## @rjsf/shadcn - Updated the test mocks to remove `formContext` for the widget mock and added `globalFormOptions` in the registry mock -- Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones +- BREAKING CHANGE: Updated `FieldTemplate`, `ObjectFieldTemplate` and `WrapIfAdditionalTemplate` to rename the old `additionalProperties` interface props to the new ones - Updated `BaseInputTemplate`, `CheckboxesWidget`, `CheckboxWidget`, `SelectWidget`, and `TextareaWidget` to use `htmlName` for the HTML `name` attribute (Note: `RadioWidget` does not support `htmlName` due to Radix UI RadioGroup limitations) +- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes ## @rjsf/utils -- BREAKING CHANGE: Updated `FieldTemplateProps` and `WrapIfAdditionalTemplateProps` to replace the `onKeyChange()` and `onDropPropertyClick()` callback generator props with the `onKeyRename()`, `onKeyRenameBlur()` and `onRemoveProperty()` callback props -- BREAKING CHANGE: Updated `ObjectFieldTemplateProps` to replace the `onAddClick()` callback generator prop with the `onAddProperty()` callback prop +- BREAKING CHANGE: Updated `FieldTemplateProps` and `WrapIfAdditionalTemplateProps` to replace the `onKeyChange()` and `onDropPropertyClick()` callback-generator props with the `onKeyRename()`, `onKeyRenameBlur()` and `onRemoveProperty()` callback props +- BREAKING CHANGE: Updated `ObjectFieldTemplateProps` to replace the `onAddClick()` callback-generator prop with the `onAddProperty()` callback prop - Added new hook `useDeepCompareMemo()` and its associated tests - Added `NameGeneratorFunction` type and two built-in name generators: `bracketNameGenerator` and `dotNotationNameGenerator` - Updated `GlobalFormOptions` type to include optional `nameGenerator` field - Updated `toFieldPathId()` function to support name generation via the `nameGenerator` option in `GlobalFormOptions` - Added `htmlName` field to `WidgetProps` interface to provide the generated HTML `name` attribute to widgets +- BREAKING CHANGE: Renamed `ArrayFieldItemTemplateType` to `ArrayFieldItemTemplateProps` and updated it to change `key: string` to `itemKey: string` to avoid a name collision with React +- BREAKING CHANGE: Updated `ArrayFieldTemplateProps` to change the type of the `items` prop from `ArrayFieldItemTemplateType[]` to `ReactElement[]` +- BREAKING CHANGE: Updated `ArrayFieldItemButtonsTemplateType` to replace the `onAddIndexClick()`, `onCopyIndexClick()`, `onDropIndexClick()` and `onReorderClick()` callback-generator props with the `onAddItem()`, `onCopyItem()`, `onMoveUpItem()`, `onMoveDownItem()` and `onRemoveItem()` callback props +- BREAKING CHANGE: Updated `ArrayFieldItemTemplateType` to change `key: string` to `itemKey: string` to avoid a name collision with React +- BREAKING CHANGE: Renamed `ArrayFieldItemButtonsTemplateType` to `ArrayFieldItemButtonsTemplateProps` and updated it to replace the `onAddIndexClick()`, `onCopyIndexClick()`, `onDropIndexClick()` and `onReorderClick()` callback-generator props with the `onAddItem()`, `onCopyItem()`, `onMoveUpItem()`, `onMoveDownItem()` and `onRemoveItem()` callback props ## Dev / docs / playground - Updated the `formTests.tsx` snapshots to add an `anyOf` of all arrays with different item types and removed the disabling of the optional data controls feature for the optional object with oneOfs @@ -98,9 +121,10 @@ should change the heading of the (upcoming) version to include a major version b - Updated the `Sample` and `LiveSettings` types to support the `liveSettings` inside of a sample - Updated the `Playground`'s `onSampleSelected` callback to merge any `liveSettings` in the sample on top of those already used in the playground - Updated the `customFieldAnyOf` sample to switch `IdSchema` to `FieldPathId` -- Updated the `custom-templates.md` documentation to reflect the `additionalProperties`-based interface props replacement -- Updated the `utility-functions.mf` documenation to add the new `useDeepCompareMemo()` hook -- Updated the `v6.x upgrade guide.md` documentation to add the BREAKING CHANGES to the `FieldTemplateProps`, `ObjectFieldTemplateProps` and `WrapIfAdditionalTemplateProps` interface props changes and the `useDeepCompareMemo()` hook +- Updated the `customArray` sample to refactor out a `ArrayFieldItemButtonsTemplate` +- Updated the `custom-templates.md` documentation to reflect the `additionalProperties`-based interface props replacement and `ArrayField` conversion changes +- Updated the `utility-functions.md` documentation to add the new `useDeepCompareMemo()` hook +- Updated the `v6.x upgrade guide.md` documentation to add the BREAKING CHANGES to the `ArrayFieldTemplateProps`, `ArrayFieldItemTemplateType`, `ArrayFieldItemButtonsTemplateType`, `FieldTemplateProps`, `ObjectFieldTemplateProps` and `WrapIfAdditionalTemplateProps` interface props changes and the `useDeepCompareMemo()` hook - Added documentation for the `nameGenerator` prop in `form-props.md` and v6.x upgrade guide # 6.0.0-beta.21 diff --git a/packages/antd/src/templates/ArrayFieldItemTemplate/index.tsx b/packages/antd/src/templates/ArrayFieldItemTemplate/index.tsx index 31fdf7edc4..9cfad610ae 100644 --- a/packages/antd/src/templates/ArrayFieldItemTemplate/index.tsx +++ b/packages/antd/src/templates/ArrayFieldItemTemplate/index.tsx @@ -1,6 +1,6 @@ import { Col, Row, Space } from 'antd'; import { - ArrayFieldItemTemplateType, + ArrayFieldItemTemplateProps, FormContextType, getUiOptions, getTemplate, @@ -18,13 +18,13 @@ const BTN_STYLE = { /** The `ArrayFieldItemTemplate` component is the template used to render an items of an array. * - * @param props - The `ArrayFieldItemTemplateType` props for the component + * @param props - The `ArrayFieldItemTemplateProps` props for the component */ export default function ArrayFieldItemTemplate< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any, ->(props: ArrayFieldItemTemplateType) { +>(props: ArrayFieldItemTemplateProps) { const { children, buttonsProps, hasToolbar, index, registry, uiSchema } = props; const uiOptions = getUiOptions(uiSchema); const ArrayFieldItemButtonsTemplate = getTemplate<'ArrayFieldItemButtonsTemplate', T, S, F>( diff --git a/packages/antd/src/templates/ArrayFieldTemplate/index.tsx b/packages/antd/src/templates/ArrayFieldTemplate/index.tsx index fe9f670c23..1dacaa1a12 100644 --- a/packages/antd/src/templates/ArrayFieldTemplate/index.tsx +++ b/packages/antd/src/templates/ArrayFieldTemplate/index.tsx @@ -2,7 +2,6 @@ import { getTemplate, getUiOptions, ArrayFieldTemplateProps, - ArrayFieldItemTemplateType, FormContextType, GenericObjectType, RJSFSchema, @@ -19,7 +18,7 @@ const DESCRIPTION_COL_STYLE = { /** The `ArrayFieldTemplate` component is the template used to render all items in an array. * - * @param props - The `ArrayFieldItemTemplateType` props for the component + * @param props - The `ArrayFieldTemplateProps` props for the component */ export default function ArrayFieldTemplate< T = any, @@ -47,11 +46,6 @@ export default function ArrayFieldTemplate< registry, uiOptions, ); - const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>( - 'ArrayFieldItemTemplate', - registry, - uiOptions, - ); const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T, S, F>( 'ArrayFieldTitleTemplate', registry, @@ -103,9 +97,7 @@ export default function ArrayFieldTemplate< )} {!showOptionalDataControlInTitle ? optionalDataControl : undefined} - {items.map(({ key, ...itemProps }: ArrayFieldItemTemplateType) => ( - - ))} + {items} {canAdd && ( diff --git a/packages/chakra-ui/src/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx b/packages/chakra-ui/src/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx index 163927bfa6..e02c06cfc0 100644 --- a/packages/chakra-ui/src/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx +++ b/packages/chakra-ui/src/ArrayFieldItemTemplate/ArrayFieldItemTemplate.tsx @@ -1,6 +1,6 @@ import { Box, ButtonGroup, HStack } from '@chakra-ui/react'; import { - ArrayFieldItemTemplateType, + ArrayFieldItemTemplateProps, FormContextType, getTemplate, getUiOptions, @@ -12,7 +12,7 @@ export default function ArrayFieldItemTemplate< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any, ->(props: ArrayFieldItemTemplateType) { +>(props: ArrayFieldItemTemplateProps) { const { children, buttonsProps, hasToolbar, uiSchema, registry } = props; const uiOptions = getUiOptions(uiSchema); const ArrayFieldItemButtonsTemplate = getTemplate<'ArrayFieldItemButtonsTemplate', T, S, F>( diff --git a/packages/chakra-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx b/packages/chakra-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx index 227f2fa423..413c63c295 100644 --- a/packages/chakra-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx +++ b/packages/chakra-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx @@ -2,7 +2,6 @@ import { Box, Grid, GridItem } from '@chakra-ui/react'; import { getTemplate, getUiOptions, - ArrayFieldItemTemplateType, ArrayFieldTemplateProps, StrictRJSFSchema, RJSFSchema, @@ -35,11 +34,6 @@ export default function ArrayFieldTemplate< registry, uiOptions, ); - const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>( - 'ArrayFieldItemTemplate', - registry, - uiOptions, - ); const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T, S, F>( 'ArrayFieldTitleTemplate', registry, @@ -71,9 +65,7 @@ export default function ArrayFieldTemplate< {!showOptionalDataControlInTitle ? optionalDataControl : undefined} - {items.map(({ key, ...itemProps }: ArrayFieldItemTemplateType) => ( - - ))} + {items} {canAdd && ( diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 2fe5faa91f..e0353d670c 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -862,7 +862,11 @@ export default class Form< customErrors = new ErrorSchemaBuilder(); } if (isRootPath) { - customErrors.setErrors(_get(newErrorSchema, ERRORS_KEY, '')); + const errors = _get(newErrorSchema, ERRORS_KEY); + if (errors) { + // only set errors when there are some + customErrors.setErrors(errors); + } } else { _set(customErrors.ErrorSchema, path, newErrorSchema); } diff --git a/packages/core/src/components/fields/ArrayField.tsx b/packages/core/src/components/fields/ArrayField.tsx index 2feba937e1..b768c4cff2 100644 --- a/packages/core/src/components/fields/ArrayField.tsx +++ b/packages/core/src/components/fields/ArrayField.tsx @@ -1,29 +1,33 @@ -import { Component, MouseEvent } from 'react'; +import { MouseEvent, useCallback, useMemo, useState } from 'react'; import { + allowAdditionalItems, getTemplate, - getWidget, getUiOptions, - isFixedItems, - allowAdditionalItems, + getWidget, + hashObject, isCustomWidget, + isFixedItems, isFormDataAvailable, optionsList, shouldRenderOptionalField, toFieldPathId, + useDeepCompareMemo, + ITEMS_KEY, + ID_KEY, ArrayFieldTemplateProps, ErrorSchema, FieldPathId, FieldPathList, FieldProps, FormContextType, + Registry, RJSFSchema, StrictRJSFSchema, TranslatableString, UiSchema, - ITEMS_KEY, + UIOptionsType, } from '@rjsf/utils'; import cloneDeep from 'lodash/cloneDeep'; -import get from 'lodash/get'; import isObject from 'lodash/isObject'; import set from 'lodash/set'; import uniqueId from 'lodash/uniqueId'; @@ -31,14 +35,6 @@ import uniqueId from 'lodash/uniqueId'; /** Type used to represent the keyed form data used in the state */ type KeyedFormDataType = { key: string; item: T }; -/** Type used for the state of the `ArrayField` component */ -type ArrayFieldState = { - /** The keyed form data elements */ - keyedFormData: KeyedFormDataType[]; - /** Flag indicating whether any of the keyed form data has been updated */ - updatedKeyedFormData: boolean; -}; - /** Used to generate a unique ID for an element in a row */ function generateRowId() { return uniqueId('rjsf-array-item-'); @@ -72,124 +68,773 @@ function keyedToPlainFormData(keyedFormData: KeyedFormDataType | KeyedForm return []; } -/** 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. +/** 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 */ -class ArrayField extends Component< - FieldProps, - ArrayFieldState -> { - /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData` - * - * @param props - The `FieldProps` for this template - */ - constructor(props: FieldProps) { - super(props); - const { formData } = props; - const keyedFormData = generateKeyedFormData(formData); - this.state = { - keyedFormData, - updatedKeyedFormData: false, - }; +function isItemRequired(itemSchema: S) { + 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'; +} - /** React lifecycle method that is called when the props are about to change allowing the state to be updated. It - * regenerates the keyed form data and returns it - * - * @param nextProps - The next set of props data - * @param prevState - The previous set of state data - */ - static getDerivedStateFromProps( - nextProps: Readonly>, - prevState: Readonly>, - ) { - // Don't call getDerivedStateFromProps if keyed formdata was just updated. - if (prevState.updatedKeyedFormData) { - return { - updatedKeyedFormData: false, - }; +/** 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: Registry, + schema: S, + formItems: T[], + uiSchema?: 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; } - const nextFormData = Array.isArray(nextProps.formData) ? nextProps.formData : []; - const previousKeyedFormData = prevState.keyedFormData || []; - const newKeyedFormData = - nextFormData.length === previousKeyedFormData.length - ? previousKeyedFormData.map((previousKeyedFormDatum, index) => { - return { - key: previousKeyedFormDatum.key, - item: nextFormData[index], - }; - }) - : generateKeyedFormData(nextFormData); - return { - keyedFormData: newKeyedFormData, - }; } + return addable; +} - /** Returns the appropriate title for an item by getting first the title from the schema.items, then falling back to - * the description from the schema.items, and finally the string "Item" - */ - get itemTitle() { - const { schema, registry } = this.props; - const { translateString } = registry; - return get( - schema, - [ITEMS_KEY, 'title'], - get(schema, [ITEMS_KEY, 'description'], translateString(TranslatableString.ArrayItemTitle)), - ); +/** 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: UiSchema, + item: T, + index: number, + formContext: F, +): UiSchema | undefined { + 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 as UiSchema; + } 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 as UiSchema | undefined; } +} - /** 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 - */ - isItemRequired(itemSchema: S) { - 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'; +/** 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: Registry, + schema: S, +): T { + const { schemaUtils } = registry; + let itemSchema = schema.items as S; + if (isFixedItems(schema) && allowAdditionalItems(schema)) { + itemSchema = schema.additionalItems as S; } + // Cast this as a T to work around schema utils being for T[] caused by the FieldProps call on the class + return schemaUtils.getDefaultFormState(itemSchema) as unknown as T; +} - /** 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 formItems - The list of items in the form - * @returns - True if the item is addable otherwise false - */ - canAddItem(formItems: any[]) { - const { schema, uiSchema, registry } = this.props; - 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; +/** Props used for ArrayAsXxxx type components*/ +interface ArrayAsFieldProps + extends FieldProps { + /** The callback used to update the array when the selector changes */ + onSelectChange: (value: T) => void; +} + +/** Renders an array as a set of checkboxes using the 'select' widget + */ +function ArrayAsMultiSelect( + props: ArrayAsFieldProps, +) { + 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 as S, 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 ( + + ); +} + +/** Renders an array using the custom widget provided by the user in the `uiSchema` + */ +function ArrayAsCustomWidget( + props: ArrayAsFieldProps, +) { + 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 ( + + ); +} + +/** Renders an array of files using the `FileWidget` + */ +function ArrayAsFiles( + props: ArrayAsFieldProps, +) { + 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 ( + + ); +} + +/** 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: { + itemKey: string; + index: number; + name: string; + disabled: boolean; + readonly: boolean; + required: boolean; + hideError: boolean; + registry: Registry; + uiOptions: UIOptionsType; + parentUiSchema?: UiSchema; + title: string | undefined; + canAdd: boolean; + canRemove?: boolean; + canMoveUp: boolean; + canMoveDown: boolean; + itemSchema: S; + itemData: T[]; + itemUiSchema: UiSchema | undefined; + itemFieldPathId: FieldPathId; + itemErrorSchema?: ErrorSchema; + autofocus?: boolean; + onBlur: FieldProps['onBlur']; + onFocus: FieldProps['onFocus']; + onChange: FieldProps['onChange']; + rawErrors?: string[]; + totalItems: number; + handleAddItem: (event: MouseEvent, index?: number) => void; + handleCopyItem: (event: MouseEvent, index: number) => void; + handleRemoveItem: (event: MouseEvent, index: number) => void; + handleReorderItems: (event: MouseEvent, index: number, newIndex: number) => void; +}) { + 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 { + fields: { ArraySchemaField, SchemaField }, + } = registry; + const fieldPathId = useDeepCompareMemo(itemFieldPathId); + const ItemSchemaField = ArraySchemaField || SchemaField; + const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T[], S, F>( + 'ArrayFieldItemTemplate', + registry, + uiOptions, + ); + const { orderable = true, removable = true, copyable = false } = uiOptions; + const has: { [key: string]: boolean } = { + moveUp: orderable && canMoveUp, + moveDown: orderable && canMoveDown, + copy: copyable && canAdd, + remove: removable && canRemove, + toolbar: false, + }; + has.toolbar = Object.keys(has).some((key: keyof typeof has) => has[key]); + + const onAddItem = useCallback( + (event: MouseEvent) => { + handleAddItem(event, index + 1); + }, + [handleAddItem, index], + ); + const onCopyItem = useCallback( + (event: MouseEvent) => { + handleCopyItem(event, index); + }, + [handleCopyItem, index], + ); + const onRemoveItem = useCallback( + (event: MouseEvent) => { + handleRemoveItem(event, index); + }, + [handleRemoveItem, index], + ); + const onMoveUpItem = useCallback( + (event: MouseEvent) => { + handleReorderItems(event, index, index - 1); + }, + [handleReorderItems, index], + ); + const onMoveDownItem = useCallback( + (event: MouseEvent) => { + handleReorderItems(event, index, index + 1); + }, + [handleReorderItems, index], + ); + + const templateProps = { + children: ( + (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, + }; + return ; +} + +/** The properties required by the stateless components that render the items using the `ArrayFieldItem` */ +interface InternalArrayFieldProps + extends FieldProps { + /** The keyedFormData from the `ArrayField` state */ + keyedFormData: KeyedFormDataType[]; + /** The callback used to handle the adding of an item at the given index (or the end, if missing) */ + handleAddItem: (event: MouseEvent, index?: number) => void; + /** The callback used to handle the copying of the item at the given index, below itself */ + handleCopyItem: (event: MouseEvent, index: number) => void; + /** The callback used to handle removing an item at the given index */ + handleRemoveItem: (event: MouseEvent, index: number) => void; + /** The callback used to handle reordering an item at the given index to its newIndex */ + handleReorderItems: (event: MouseEvent, index: number, newIndex: number) => void; +} + +/** Renders a normal array without any limitations of length + */ +function NormalArray( + props: InternalArrayFieldProps, +) { + 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: S = isObject(schema.items) ? (schema.items as S) : ({} as S); + const itemsSchema: S = 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' : ''; + const optionalDataControl = renderOptionalField ? : undefined; + const arrayProps: ArrayFieldTemplateProps = { + canAdd, + items: actualFormData.map((keyedItem, index: number) => { + 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 as unknown as T[]; + const itemSchema = schemaUtils.retrieveSchema(_schemaItems, itemCast); + const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema) : undefined; + const itemFieldPathId = toFieldPathId(index, globalFormOptions, fieldPathId); + + // 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 ; + }), + 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', T[], S, F>('ArrayFieldTemplate', registry, uiOptions); + return