@rjsf/core 6.0.0-beta.9 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +2 -0
  2. package/dist/core.umd.js +2042 -1987
  3. package/dist/index.cjs +4909 -0
  4. package/dist/index.cjs.map +7 -0
  5. package/dist/index.esm.js +2509 -2389
  6. package/dist/index.esm.js.map +4 -4
  7. package/lib/components/Form.d.ts +137 -34
  8. package/lib/components/Form.d.ts.map +1 -1
  9. package/lib/components/Form.js +318 -173
  10. package/lib/components/fields/ArrayField.d.ts +2 -187
  11. package/lib/components/fields/ArrayField.d.ts.map +1 -1
  12. package/lib/components/fields/ArrayField.js +526 -492
  13. package/lib/components/fields/BooleanField.d.ts.map +1 -1
  14. package/lib/components/fields/BooleanField.js +8 -3
  15. package/lib/components/fields/FallbackField.d.ts +7 -0
  16. package/lib/components/fields/FallbackField.d.ts.map +1 -0
  17. package/lib/components/fields/FallbackField.js +72 -0
  18. package/lib/components/fields/LayoutGridField.d.ts +109 -186
  19. package/lib/components/fields/LayoutGridField.d.ts.map +1 -1
  20. package/lib/components/fields/LayoutGridField.js +426 -426
  21. package/lib/components/fields/LayoutHeaderField.d.ts +1 -1
  22. package/lib/components/fields/LayoutHeaderField.js +3 -3
  23. package/lib/components/fields/LayoutMultiSchemaField.d.ts.map +1 -1
  24. package/lib/components/fields/LayoutMultiSchemaField.js +6 -6
  25. package/lib/components/fields/MultiSchemaField.d.ts.map +1 -1
  26. package/lib/components/fields/MultiSchemaField.js +16 -10
  27. package/lib/components/fields/NullField.js +3 -3
  28. package/lib/components/fields/NumberField.d.ts.map +1 -1
  29. package/lib/components/fields/NumberField.js +3 -3
  30. package/lib/components/fields/ObjectField.d.ts +2 -68
  31. package/lib/components/fields/ObjectField.d.ts.map +1 -1
  32. package/lib/components/fields/ObjectField.js +163 -163
  33. package/lib/components/fields/OptionalDataControlsField.d.ts +8 -0
  34. package/lib/components/fields/OptionalDataControlsField.d.ts.map +1 -0
  35. package/lib/components/fields/OptionalDataControlsField.js +43 -0
  36. package/lib/components/fields/SchemaField.d.ts.map +1 -1
  37. package/lib/components/fields/SchemaField.js +52 -30
  38. package/lib/components/fields/StringField.d.ts.map +1 -1
  39. package/lib/components/fields/StringField.js +8 -3
  40. package/lib/components/fields/index.d.ts.map +1 -1
  41. package/lib/components/fields/index.js +4 -0
  42. package/lib/components/templates/ArrayFieldDescriptionTemplate.d.ts +1 -1
  43. package/lib/components/templates/ArrayFieldDescriptionTemplate.js +3 -3
  44. package/lib/components/templates/ArrayFieldItemButtonsTemplate.d.ts +3 -3
  45. package/lib/components/templates/ArrayFieldItemButtonsTemplate.d.ts.map +1 -1
  46. package/lib/components/templates/ArrayFieldItemButtonsTemplate.js +3 -8
  47. package/lib/components/templates/ArrayFieldItemTemplate.d.ts +3 -3
  48. package/lib/components/templates/ArrayFieldItemTemplate.d.ts.map +1 -1
  49. package/lib/components/templates/ArrayFieldItemTemplate.js +1 -1
  50. package/lib/components/templates/ArrayFieldTemplate.d.ts +1 -1
  51. package/lib/components/templates/ArrayFieldTemplate.d.ts.map +1 -1
  52. package/lib/components/templates/ArrayFieldTemplate.js +4 -5
  53. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts +1 -1
  54. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts.map +1 -1
  55. package/lib/components/templates/ArrayFieldTitleTemplate.js +3 -3
  56. package/lib/components/templates/BaseInputTemplate.js +2 -2
  57. package/lib/components/templates/ButtonTemplates/AddButton.d.ts +1 -1
  58. package/lib/components/templates/ButtonTemplates/AddButton.d.ts.map +1 -1
  59. package/lib/components/templates/ButtonTemplates/AddButton.js +2 -2
  60. package/lib/components/templates/FallbackFieldTemplate.d.ts +7 -0
  61. package/lib/components/templates/FallbackFieldTemplate.d.ts.map +1 -0
  62. package/lib/components/templates/FallbackFieldTemplate.js +12 -0
  63. package/lib/components/templates/FieldErrorTemplate.js +2 -2
  64. package/lib/components/templates/FieldHelpTemplate.js +2 -2
  65. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts +8 -0
  66. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts.map +1 -0
  67. package/lib/components/templates/MultiSchemaFieldTemplate.js +10 -0
  68. package/lib/components/templates/ObjectFieldTemplate.d.ts.map +1 -1
  69. package/lib/components/templates/ObjectFieldTemplate.js +3 -2
  70. package/lib/components/templates/OptionalDataControlsTemplate.d.ts +11 -0
  71. package/lib/components/templates/OptionalDataControlsTemplate.d.ts.map +1 -0
  72. package/lib/components/templates/OptionalDataControlsTemplate.js +20 -0
  73. package/lib/components/templates/TitleField.d.ts.map +1 -1
  74. package/lib/components/templates/TitleField.js +2 -2
  75. package/lib/components/templates/UnsupportedField.js +3 -3
  76. package/lib/components/templates/WrapIfAdditionalTemplate.js +2 -2
  77. package/lib/components/templates/index.d.ts.map +1 -1
  78. package/lib/components/templates/index.js +6 -0
  79. package/lib/components/widgets/AltDateWidget.d.ts +1 -1
  80. package/lib/components/widgets/AltDateWidget.d.ts.map +1 -1
  81. package/lib/components/widgets/AltDateWidget.js +5 -46
  82. package/lib/components/widgets/CheckboxWidget.d.ts +1 -1
  83. package/lib/components/widgets/CheckboxWidget.d.ts.map +1 -1
  84. package/lib/components/widgets/CheckboxWidget.js +2 -2
  85. package/lib/components/widgets/CheckboxesWidget.d.ts +1 -1
  86. package/lib/components/widgets/CheckboxesWidget.d.ts.map +1 -1
  87. package/lib/components/widgets/CheckboxesWidget.js +4 -4
  88. package/lib/components/widgets/FileWidget.d.ts.map +1 -1
  89. package/lib/components/widgets/FileWidget.js +7 -87
  90. package/lib/components/widgets/HiddenWidget.d.ts +1 -1
  91. package/lib/components/widgets/HiddenWidget.d.ts.map +1 -1
  92. package/lib/components/widgets/HiddenWidget.js +2 -2
  93. package/lib/components/widgets/RadioWidget.d.ts +1 -1
  94. package/lib/components/widgets/RadioWidget.d.ts.map +1 -1
  95. package/lib/components/widgets/RadioWidget.js +2 -2
  96. package/lib/components/widgets/RatingWidget.d.ts +1 -1
  97. package/lib/components/widgets/RatingWidget.d.ts.map +1 -1
  98. package/lib/components/widgets/RatingWidget.js +2 -2
  99. package/lib/components/widgets/SelectWidget.d.ts +1 -1
  100. package/lib/components/widgets/SelectWidget.d.ts.map +1 -1
  101. package/lib/components/widgets/SelectWidget.js +2 -2
  102. package/lib/components/widgets/TextareaWidget.d.ts +1 -1
  103. package/lib/components/widgets/TextareaWidget.d.ts.map +1 -1
  104. package/lib/components/widgets/TextareaWidget.js +2 -2
  105. package/lib/getDefaultRegistry.d.ts.map +1 -1
  106. package/lib/getDefaultRegistry.js +6 -1
  107. package/lib/getTestRegistry.d.ts +5 -0
  108. package/lib/getTestRegistry.d.ts.map +1 -0
  109. package/lib/getTestRegistry.js +23 -0
  110. package/lib/index.d.ts +2 -1
  111. package/lib/index.d.ts.map +1 -1
  112. package/lib/index.js +2 -1
  113. package/lib/tsconfig.tsbuildinfo +1 -1
  114. package/package.json +35 -20
  115. package/src/components/Form.tsx +468 -206
  116. package/src/components/fields/ArrayField.tsx +871 -723
  117. package/src/components/fields/BooleanField.tsx +14 -5
  118. package/src/components/fields/FallbackField.tsx +157 -0
  119. package/src/components/fields/LayoutGridField.tsx +626 -603
  120. package/src/components/fields/LayoutHeaderField.tsx +3 -3
  121. package/src/components/fields/LayoutMultiSchemaField.tsx +9 -10
  122. package/src/components/fields/MultiSchemaField.tsx +57 -36
  123. package/src/components/fields/NullField.tsx +3 -3
  124. package/src/components/fields/NumberField.tsx +11 -3
  125. package/src/components/fields/ObjectField.tsx +308 -239
  126. package/src/components/fields/OptionalDataControlsField.tsx +84 -0
  127. package/src/components/fields/SchemaField.tsx +75 -94
  128. package/src/components/fields/StringField.tsx +14 -5
  129. package/src/components/fields/index.ts +4 -0
  130. package/src/components/templates/ArrayFieldDescriptionTemplate.tsx +3 -3
  131. package/src/components/templates/ArrayFieldItemButtonsTemplate.tsx +16 -21
  132. package/src/components/templates/ArrayFieldItemTemplate.tsx +3 -3
  133. package/src/components/templates/ArrayFieldTemplate.tsx +11 -18
  134. package/src/components/templates/ArrayFieldTitleTemplate.tsx +4 -3
  135. package/src/components/templates/BaseInputTemplate.tsx +5 -5
  136. package/src/components/templates/ButtonTemplates/AddButton.tsx +2 -0
  137. package/src/components/templates/FallbackFieldTemplate.tsx +28 -0
  138. package/src/components/templates/FieldErrorTemplate.tsx +2 -2
  139. package/src/components/templates/FieldHelpTemplate.tsx +2 -2
  140. package/src/components/templates/MultiSchemaFieldTemplate.tsx +20 -0
  141. package/src/components/templates/ObjectFieldTemplate.tsx +12 -7
  142. package/src/components/templates/OptionalDataControlsTemplate.tsx +43 -0
  143. package/src/components/templates/TitleField.tsx +6 -1
  144. package/src/components/templates/UnsupportedField.tsx +3 -3
  145. package/src/components/templates/WrapIfAdditionalTemplate.tsx +5 -5
  146. package/src/components/templates/index.ts +6 -0
  147. package/src/components/widgets/AltDateWidget.tsx +8 -126
  148. package/src/components/widgets/CheckboxWidget.tsx +4 -3
  149. package/src/components/widgets/CheckboxesWidget.tsx +5 -4
  150. package/src/components/widgets/FileWidget.tsx +11 -102
  151. package/src/components/widgets/HiddenWidget.tsx +2 -1
  152. package/src/components/widgets/RadioWidget.tsx +3 -2
  153. package/src/components/widgets/RatingWidget.tsx +2 -1
  154. package/src/components/widgets/SelectWidget.tsx +3 -2
  155. package/src/components/widgets/TextareaWidget.tsx +3 -2
  156. package/src/getDefaultRegistry.ts +14 -1
  157. package/src/getTestRegistry.tsx +38 -0
  158. package/src/index.ts +2 -1
  159. package/dist/index.js +0 -4834
  160. package/dist/index.js.map +0 -7
@@ -1,43 +1,43 @@
1
- import { Component, MouseEvent } from 'react';
1
+ import { MouseEvent, useCallback, useMemo, useState } from 'react';
2
2
  import {
3
+ allowAdditionalItems,
3
4
  getTemplate,
4
- getWidget,
5
5
  getUiOptions,
6
- isFixedItems,
7
- allowAdditionalItems,
6
+ getWidget,
7
+ hashObject,
8
8
  isCustomWidget,
9
+ isFixedItems,
10
+ isFormDataAvailable,
9
11
  optionsList,
12
+ shouldRenderOptionalField,
13
+ toFieldPathId,
14
+ useDeepCompareMemo,
15
+ ITEMS_KEY,
16
+ ID_KEY,
10
17
  ArrayFieldTemplateProps,
11
18
  ErrorSchema,
19
+ FieldPathId,
20
+ FieldPathList,
12
21
  FieldProps,
13
22
  FormContextType,
14
- IdSchema,
23
+ Registry,
15
24
  RJSFSchema,
16
25
  StrictRJSFSchema,
17
26
  TranslatableString,
18
27
  UiSchema,
19
- ITEMS_KEY,
28
+ UIOptionsType,
20
29
  } from '@rjsf/utils';
21
30
  import cloneDeep from 'lodash/cloneDeep';
22
- import get from 'lodash/get';
23
31
  import isObject from 'lodash/isObject';
24
32
  import set from 'lodash/set';
25
- import { nanoid } from 'nanoid';
33
+ import uniqueId from 'lodash/uniqueId';
26
34
 
27
35
  /** Type used to represent the keyed form data used in the state */
28
36
  type KeyedFormDataType<T> = { key: string; item: T };
29
37
 
30
- /** Type used for the state of the `ArrayField` component */
31
- type ArrayFieldState<T> = {
32
- /** The keyed form data elements */
33
- keyedFormData: KeyedFormDataType<T>[];
34
- /** Flag indicating whether any of the keyed form data has been updated */
35
- updatedKeyedFormData: boolean;
36
- };
37
-
38
38
  /** Used to generate a unique ID for an element in a row */
39
39
  function generateRowId() {
40
- return nanoid();
40
+ return uniqueId('rjsf-array-item-');
41
41
  }
42
42
 
43
43
  /** Converts the `formData` into `KeyedFormDataType` data, using the `generateRowId()` function to create the key
@@ -45,7 +45,7 @@ function generateRowId() {
45
45
  * @param formData - The data for the form
46
46
  * @returns - The `formData` converted into a `KeyedFormDataType` element
47
47
  */
48
- function generateKeyedFormData<T>(formData: T[]): KeyedFormDataType<T>[] {
48
+ function generateKeyedFormData<T>(formData?: T[]): KeyedFormDataType<T>[] {
49
49
  return !Array.isArray(formData)
50
50
  ? []
51
51
  : formData.map((item) => {
@@ -68,124 +68,783 @@ function keyedToPlainFormData<T>(keyedFormData: KeyedFormDataType<T> | KeyedForm
68
68
  return [];
69
69
  }
70
70
 
71
- /** The `ArrayField` component is used to render a field in the schema that is of type `array`. It supports both normal
72
- * and fixed array, allowing user to add and remove elements from the array data.
71
+ /** Determines whether the item described in the schema is always required, which is determined by whether any item
72
+ * may be null.
73
+ *
74
+ * @param itemSchema - The schema for the item
75
+ * @return - True if the item schema type does not contain the "null" type
73
76
  */
74
- class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> extends Component<
75
- FieldProps<T[], S, F>,
76
- ArrayFieldState<T>
77
- > {
78
- /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData`
79
- *
80
- * @param props - The `FieldProps` for this template
81
- */
82
- constructor(props: FieldProps<T[], S, F>) {
83
- super(props);
84
- const { formData = [] } = props;
85
- const keyedFormData = generateKeyedFormData<T>(formData);
86
- this.state = {
87
- keyedFormData,
88
- updatedKeyedFormData: false,
89
- };
77
+ function isItemRequired<S extends StrictRJSFSchema = RJSFSchema>(itemSchema: S) {
78
+ if (Array.isArray(itemSchema.type)) {
79
+ // While we don't yet support composite/nullable jsonschema types, it's
80
+ // future-proof to check for requirement against these.
81
+ return !itemSchema.type.includes('null');
90
82
  }
83
+ // All non-null array item types are inherently required by design
84
+ return itemSchema.type !== 'null';
85
+ }
91
86
 
92
- /** React lifecycle method that is called when the props are about to change allowing the state to be updated. It
93
- * regenerates the keyed form data and returns it
94
- *
95
- * @param nextProps - The next set of props data
96
- * @param prevState - The previous set of state data
97
- */
98
- static getDerivedStateFromProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
99
- nextProps: Readonly<FieldProps<T[], S, F>>,
100
- prevState: Readonly<ArrayFieldState<T>>,
101
- ) {
102
- // Don't call getDerivedStateFromProps if keyed formdata was just updated.
103
- if (prevState.updatedKeyedFormData) {
104
- return {
105
- updatedKeyedFormData: false,
106
- };
87
+ /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding
88
+ * then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the
89
+ * `formData` matches that value, then false is returned, otherwise true is returned.
90
+ *
91
+ * @param registry - The registry
92
+ * @param schema - The schema for the field
93
+ * @param formItems - The list of items in the form
94
+ * @param [uiSchema] - The UiSchema for the field
95
+ * @returns - True if the item is addable otherwise false
96
+ */
97
+ function canAddItem<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
98
+ registry: Registry<T[], S, F>,
99
+ schema: S,
100
+ formItems: T[],
101
+ uiSchema?: UiSchema<T[], S, F>,
102
+ ) {
103
+ let { addable } = getUiOptions<T[], S, F>(uiSchema, registry.globalUiOptions);
104
+ if (addable !== false) {
105
+ // if ui:options.addable was not explicitly set to false, we can add
106
+ // another item if we have not exceeded maxItems yet
107
+ if (schema.maxItems !== undefined) {
108
+ addable = formItems.length < schema.maxItems;
109
+ } else {
110
+ addable = true;
107
111
  }
108
- const nextFormData = Array.isArray(nextProps.formData) ? nextProps.formData : [];
109
- const previousKeyedFormData = prevState.keyedFormData || [];
110
- const newKeyedFormData =
111
- nextFormData.length === previousKeyedFormData.length
112
- ? previousKeyedFormData.map((previousKeyedFormDatum, index) => {
113
- return {
114
- key: previousKeyedFormDatum.key,
115
- item: nextFormData[index],
116
- };
117
- })
118
- : generateKeyedFormData<T>(nextFormData);
119
- return {
120
- keyedFormData: newKeyedFormData,
121
- };
122
112
  }
113
+ return addable;
114
+ }
123
115
 
124
- /** Returns the appropriate title for an item by getting first the title from the schema.items, then falling back to
125
- * the description from the schema.items, and finally the string "Item"
126
- */
127
- get itemTitle() {
128
- const { schema, registry } = this.props;
129
- const { translateString } = registry;
130
- return get(
131
- schema,
132
- [ITEMS_KEY, 'title'],
133
- get(schema, [ITEMS_KEY, 'description'], translateString(TranslatableString.ArrayItemTitle)),
134
- );
116
+ /** Helper method to compute item UI schema for both normal and fixed arrays
117
+ * Handles both static object and dynamic function cases
118
+ *
119
+ * @param uiSchema - The parent UI schema containing items definition
120
+ * @param item - The item data
121
+ * @param index - The index of the item
122
+ * @param formContext - The form context
123
+ * @returns The computed UI schema for the item
124
+ */
125
+ function computeItemUiSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
126
+ uiSchema: UiSchema<T[], S, F>,
127
+ item: T,
128
+ index: number,
129
+ formContext: F,
130
+ ): UiSchema<T[], S, F> | undefined {
131
+ if (typeof uiSchema.items === 'function') {
132
+ try {
133
+ // Call the function with item data, index, and form context
134
+ // TypeScript now correctly infers the types thanks to the ArrayElement type in UiSchema
135
+ const result = uiSchema.items(item, index, formContext);
136
+ // Only use the result if it's truthy
137
+ return result as UiSchema<T[], S, F>;
138
+ } catch (e) {
139
+ console.error(`Error executing dynamic uiSchema.items function for item at index ${index}:`, e);
140
+ // Fall back to undefined to allow the field to still render
141
+ return undefined;
142
+ }
143
+ } else {
144
+ // Static object case - preserve undefined to maintain backward compatibility
145
+ return uiSchema.items as UiSchema<T[], S, F> | undefined;
135
146
  }
147
+ }
136
148
 
137
- /** Determines whether the item described in the schema is always required, which is determined by whether any item
138
- * may be null.
139
- *
140
- * @param itemSchema - The schema for the item
141
- * @return - True if the item schema type does not contain the "null" type
142
- */
143
- isItemRequired(itemSchema: S) {
144
- if (Array.isArray(itemSchema.type)) {
145
- // While we don't yet support composite/nullable jsonschema types, it's
146
- // future-proof to check for requirement against these.
147
- return !itemSchema.type.includes('null');
148
- }
149
- // All non-null array item types are inherently required by design
150
- return itemSchema.type !== 'null';
149
+ /** Returns the default form information for an item based on the schema for that item. Deals with the possibility
150
+ * that the schema is fixed and allows additional items.
151
+ */
152
+ function getNewFormDataRow<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
153
+ registry: Registry<T[], S, F>,
154
+ schema: S,
155
+ ): T {
156
+ const { schemaUtils } = registry;
157
+ let itemSchema = schema.items as S;
158
+ if (isFixedItems(schema) && allowAdditionalItems(schema)) {
159
+ itemSchema = schema.additionalItems as S;
151
160
  }
161
+ // Cast this as a T to work around schema utils being for T[] caused by the FieldProps<T[], S, F> call on the class
162
+ return schemaUtils.getDefaultFormState(itemSchema) as unknown as T;
163
+ }
152
164
 
153
- /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding
154
- * then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the
155
- * `formData` matches that value, then false is returned, otherwise true is returned.
156
- *
157
- * @param formItems - The list of items in the form
158
- * @returns - True if the item is addable otherwise false
159
- */
160
- canAddItem(formItems: any[]) {
161
- const { schema, uiSchema, registry } = this.props;
162
- let { addable } = getUiOptions<T[], S, F>(uiSchema, registry.globalUiOptions);
163
- if (addable !== false) {
164
- // if ui:options.addable was not explicitly set to false, we can add
165
- // another item if we have not exceeded maxItems yet
166
- if (schema.maxItems !== undefined) {
167
- addable = formItems.length < schema.maxItems;
165
+ /** Props used for ArrayAsXxxx type components*/
166
+ interface ArrayAsFieldProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
167
+ extends FieldProps<T, S, F> {
168
+ /** The callback used to update the array when the selector changes */
169
+ onSelectChange: (value: T) => void;
170
+ }
171
+
172
+ /** Renders an array as a set of checkboxes using the 'select' widget
173
+ */
174
+ function ArrayAsMultiSelect<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
175
+ props: ArrayAsFieldProps<T[], S, F>,
176
+ ) {
177
+ const {
178
+ schema,
179
+ fieldPathId,
180
+ uiSchema,
181
+ formData: items = [],
182
+ disabled = false,
183
+ readonly = false,
184
+ autofocus = false,
185
+ required = false,
186
+ placeholder,
187
+ onBlur,
188
+ onFocus,
189
+ registry,
190
+ rawErrors,
191
+ name,
192
+ onSelectChange,
193
+ } = props;
194
+ const { widgets, schemaUtils, globalFormOptions, globalUiOptions } = registry;
195
+ const itemsSchema = schemaUtils.retrieveSchema(schema.items as S, items);
196
+ const enumOptions = optionsList<T[], S, F>(itemsSchema, uiSchema);
197
+ const { widget = 'select', title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
198
+ const Widget = getWidget<T[], S, F>(schema, widget, widgets);
199
+ const label = uiTitle ?? schema.title ?? name;
200
+ const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
201
+ // For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
202
+ const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));
203
+ return (
204
+ <Widget
205
+ id={multiValueFieldPathId[ID_KEY]}
206
+ name={name}
207
+ multiple
208
+ onChange={onSelectChange}
209
+ onBlur={onBlur}
210
+ onFocus={onFocus}
211
+ options={{ ...options, enumOptions }}
212
+ schema={schema}
213
+ uiSchema={uiSchema}
214
+ registry={registry}
215
+ value={items}
216
+ disabled={disabled}
217
+ readonly={readonly}
218
+ required={required}
219
+ label={label}
220
+ hideLabel={!displayLabel}
221
+ placeholder={placeholder}
222
+ autofocus={autofocus}
223
+ rawErrors={rawErrors}
224
+ htmlName={multiValueFieldPathId.name}
225
+ />
226
+ );
227
+ }
228
+
229
+ /** Renders an array using the custom widget provided by the user in the `uiSchema`
230
+ */
231
+ function ArrayAsCustomWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
232
+ props: ArrayAsFieldProps<T[], S, F>,
233
+ ) {
234
+ const {
235
+ schema,
236
+ fieldPathId,
237
+ uiSchema,
238
+ disabled = false,
239
+ readonly = false,
240
+ autofocus = false,
241
+ required = false,
242
+ hideError,
243
+ placeholder,
244
+ onBlur,
245
+ onFocus,
246
+ formData: items = [],
247
+ registry,
248
+ rawErrors,
249
+ name,
250
+ onSelectChange,
251
+ } = props;
252
+ const { widgets, schemaUtils, globalFormOptions, globalUiOptions } = registry;
253
+ const { widget, title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
254
+ const Widget = getWidget<T[], S, F>(schema, widget, widgets);
255
+ const label = uiTitle ?? schema.title ?? name;
256
+ const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
257
+ // For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
258
+ const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));
259
+ return (
260
+ <Widget
261
+ id={multiValueFieldPathId[ID_KEY]}
262
+ name={name}
263
+ multiple
264
+ onChange={onSelectChange}
265
+ onBlur={onBlur}
266
+ onFocus={onFocus}
267
+ options={options}
268
+ schema={schema}
269
+ uiSchema={uiSchema}
270
+ registry={registry}
271
+ value={items}
272
+ disabled={disabled}
273
+ readonly={readonly}
274
+ hideError={hideError}
275
+ required={required}
276
+ label={label}
277
+ hideLabel={!displayLabel}
278
+ placeholder={placeholder}
279
+ autofocus={autofocus}
280
+ rawErrors={rawErrors}
281
+ htmlName={multiValueFieldPathId.name}
282
+ />
283
+ );
284
+ }
285
+
286
+ /** Renders an array of files using the `FileWidget`
287
+ */
288
+ function ArrayAsFiles<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
289
+ props: ArrayAsFieldProps<T[], S, F>,
290
+ ) {
291
+ const {
292
+ schema,
293
+ uiSchema,
294
+ fieldPathId,
295
+ name,
296
+ disabled = false,
297
+ readonly = false,
298
+ autofocus = false,
299
+ required = false,
300
+ onBlur,
301
+ onFocus,
302
+ registry,
303
+ formData: items = [],
304
+ rawErrors,
305
+ onSelectChange,
306
+ } = props;
307
+ const { widgets, schemaUtils, globalFormOptions, globalUiOptions } = registry;
308
+ const { widget = 'files', title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
309
+ const Widget = getWidget<T[], S, F>(schema, widget, widgets);
310
+ const label = uiTitle ?? schema.title ?? name;
311
+ const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
312
+ // For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
313
+ const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));
314
+ return (
315
+ <Widget
316
+ options={options}
317
+ id={multiValueFieldPathId[ID_KEY]}
318
+ name={name}
319
+ multiple
320
+ onChange={onSelectChange}
321
+ onBlur={onBlur}
322
+ onFocus={onFocus}
323
+ schema={schema}
324
+ uiSchema={uiSchema}
325
+ value={items}
326
+ disabled={disabled}
327
+ readonly={readonly}
328
+ required={required}
329
+ registry={registry}
330
+ autofocus={autofocus}
331
+ rawErrors={rawErrors}
332
+ label={label}
333
+ hideLabel={!displayLabel}
334
+ htmlName={multiValueFieldPathId.name}
335
+ />
336
+ );
337
+ }
338
+
339
+ /** Renders the individual array item using a `SchemaField` along with the additional properties that are needed to
340
+ * render the whole of the `ArrayFieldItemTemplate`.
341
+ */
342
+ function ArrayFieldItem<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(props: {
343
+ itemKey: string;
344
+ index: number;
345
+ name: string;
346
+ disabled: boolean;
347
+ readonly: boolean;
348
+ required: boolean;
349
+ hideError: boolean;
350
+ registry: Registry<T[], S, F>;
351
+ uiOptions: UIOptionsType<T[], S, F>;
352
+ parentUiSchema?: UiSchema<T[], S, F>;
353
+ title: string | undefined;
354
+ canAdd: boolean;
355
+ canRemove?: boolean;
356
+ canMoveUp: boolean;
357
+ canMoveDown: boolean;
358
+ itemSchema: S;
359
+ itemData: T[];
360
+ itemUiSchema: UiSchema<T[], S, F> | undefined;
361
+ itemFieldPathId: FieldPathId;
362
+ itemErrorSchema?: ErrorSchema<T[]>;
363
+ autofocus?: boolean;
364
+ onBlur: FieldProps<T[], S, F>['onBlur'];
365
+ onFocus: FieldProps<T[], S, F>['onFocus'];
366
+ onChange: FieldProps<T[], S, F>['onChange'];
367
+ rawErrors?: string[];
368
+ totalItems: number;
369
+ handleAddItem: (event: MouseEvent, index?: number) => void;
370
+ handleCopyItem: (event: MouseEvent, index: number) => void;
371
+ handleRemoveItem: (event: MouseEvent, index: number) => void;
372
+ handleReorderItems: (event: MouseEvent<HTMLButtonElement>, index: number, newIndex: number) => void;
373
+ }) {
374
+ const {
375
+ itemKey,
376
+ index,
377
+ name,
378
+ disabled,
379
+ hideError,
380
+ readonly,
381
+ registry,
382
+ uiOptions,
383
+ parentUiSchema,
384
+ canAdd,
385
+ canRemove = true,
386
+ canMoveUp,
387
+ canMoveDown,
388
+ itemSchema,
389
+ itemData,
390
+ itemUiSchema,
391
+ itemFieldPathId,
392
+ itemErrorSchema,
393
+ autofocus,
394
+ onBlur,
395
+ onFocus,
396
+ onChange,
397
+ rawErrors,
398
+ totalItems,
399
+ title,
400
+ handleAddItem,
401
+ handleCopyItem,
402
+ handleRemoveItem,
403
+ handleReorderItems,
404
+ } = props;
405
+ const {
406
+ fields: { ArraySchemaField, SchemaField },
407
+ } = registry;
408
+ const fieldPathId = useDeepCompareMemo<FieldPathId>(itemFieldPathId);
409
+ const ItemSchemaField = ArraySchemaField || SchemaField;
410
+ const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T[], S, F>(
411
+ 'ArrayFieldItemTemplate',
412
+ registry,
413
+ uiOptions,
414
+ );
415
+ const { orderable = true, removable = true, copyable = false } = uiOptions;
416
+ const has: { [key: string]: boolean } = {
417
+ moveUp: orderable && canMoveUp,
418
+ moveDown: orderable && canMoveDown,
419
+ copy: copyable && canAdd,
420
+ remove: removable && canRemove,
421
+ toolbar: false,
422
+ };
423
+ has.toolbar = Object.keys(has).some((key: keyof typeof has) => has[key]);
424
+
425
+ const onAddItem = useCallback(
426
+ (event: MouseEvent) => {
427
+ handleAddItem(event, index + 1);
428
+ },
429
+ [handleAddItem, index],
430
+ );
431
+ const onCopyItem = useCallback(
432
+ (event: MouseEvent) => {
433
+ handleCopyItem(event, index);
434
+ },
435
+ [handleCopyItem, index],
436
+ );
437
+ const onRemoveItem = useCallback(
438
+ (event: MouseEvent) => {
439
+ handleRemoveItem(event, index);
440
+ },
441
+ [handleRemoveItem, index],
442
+ );
443
+ const onMoveUpItem = useCallback(
444
+ (event: MouseEvent<HTMLButtonElement>) => {
445
+ handleReorderItems(event, index, index - 1);
446
+ },
447
+ [handleReorderItems, index],
448
+ );
449
+ const onMoveDownItem = useCallback(
450
+ (event: MouseEvent<HTMLButtonElement>) => {
451
+ handleReorderItems(event, index, index + 1);
452
+ },
453
+ [handleReorderItems, index],
454
+ );
455
+
456
+ const templateProps = {
457
+ children: (
458
+ <ItemSchemaField
459
+ name={name}
460
+ title={title}
461
+ index={index}
462
+ schema={itemSchema}
463
+ uiSchema={itemUiSchema}
464
+ formData={itemData}
465
+ errorSchema={itemErrorSchema}
466
+ fieldPathId={fieldPathId}
467
+ required={isItemRequired<S>(itemSchema)}
468
+ onChange={onChange}
469
+ onBlur={onBlur}
470
+ onFocus={onFocus}
471
+ registry={registry}
472
+ disabled={disabled}
473
+ readonly={readonly}
474
+ hideError={hideError}
475
+ autofocus={autofocus}
476
+ rawErrors={rawErrors}
477
+ />
478
+ ),
479
+ buttonsProps: {
480
+ fieldPathId,
481
+ disabled,
482
+ readonly,
483
+ canAdd,
484
+ hasCopy: has.copy,
485
+ hasMoveUp: has.moveUp,
486
+ hasMoveDown: has.moveDown,
487
+ hasRemove: has.remove,
488
+ index: index,
489
+ totalItems,
490
+ onAddItem,
491
+ onCopyItem,
492
+ onRemoveItem,
493
+ onMoveUpItem,
494
+ onMoveDownItem,
495
+ registry,
496
+ schema: itemSchema,
497
+ uiSchema: itemUiSchema,
498
+ },
499
+ itemKey,
500
+ className: 'rjsf-array-item',
501
+ disabled,
502
+ hasToolbar: has.toolbar,
503
+ index,
504
+ totalItems,
505
+ readonly,
506
+ registry,
507
+ schema: itemSchema,
508
+ uiSchema: itemUiSchema,
509
+ parentUiSchema,
510
+ };
511
+ return <ArrayFieldItemTemplate {...templateProps} />;
512
+ }
513
+
514
+ /** The properties required by the stateless components that render the items using the `ArrayFieldItem` */
515
+ interface InternalArrayFieldProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
516
+ extends FieldProps<T[], S, F> {
517
+ /** The keyedFormData from the `ArrayField` state */
518
+ keyedFormData: KeyedFormDataType<T>[];
519
+ /** The callback used to handle the adding of an item at the given index (or the end, if missing) */
520
+ handleAddItem: (event: MouseEvent, index?: number) => void;
521
+ /** The callback used to handle the copying of the item at the given index, below itself */
522
+ handleCopyItem: (event: MouseEvent, index: number) => void;
523
+ /** The callback used to handle removing an item at the given index */
524
+ handleRemoveItem: (event: MouseEvent, index: number) => void;
525
+ /** The callback used to handle reordering an item at the given index to its newIndex */
526
+ handleReorderItems: (event: MouseEvent<HTMLButtonElement>, index: number, newIndex: number) => void;
527
+ }
528
+
529
+ /** Renders a normal array without any limitations of length
530
+ */
531
+ function NormalArray<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
532
+ props: InternalArrayFieldProps<T, S, F>,
533
+ ) {
534
+ const {
535
+ schema,
536
+ uiSchema = {},
537
+ errorSchema,
538
+ fieldPathId,
539
+ formData: formDataFromProps,
540
+ name,
541
+ title,
542
+ disabled = false,
543
+ readonly = false,
544
+ autofocus = false,
545
+ required = false,
546
+ hideError = false,
547
+ registry,
548
+ onBlur,
549
+ onFocus,
550
+ rawErrors,
551
+ onChange,
552
+ keyedFormData,
553
+ handleAddItem,
554
+ handleCopyItem,
555
+ handleRemoveItem,
556
+ handleReorderItems,
557
+ } = props;
558
+ const fieldTitle = schema.title || title || name;
559
+ const { schemaUtils, fields, formContext, globalFormOptions, globalUiOptions } = registry;
560
+ const { OptionalDataControlsField } = fields;
561
+ const uiOptions = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
562
+ const _schemaItems: S = isObject(schema.items) ? (schema.items as S) : ({} as S);
563
+ const itemsSchema: S = schemaUtils.retrieveSchema(_schemaItems);
564
+ const formData = keyedToPlainFormData<T>(keyedFormData);
565
+ const renderOptionalField = shouldRenderOptionalField<T[], S, F>(registry, schema, required, uiSchema);
566
+ const hasFormData = isFormDataAvailable<T[]>(formDataFromProps);
567
+ const canAdd = canAddItem<T, S, F>(registry, schema, formData, uiSchema) && (!renderOptionalField || hasFormData);
568
+ const actualFormData = hasFormData ? keyedFormData : [];
569
+ const extraClass = renderOptionalField ? ' rjsf-optional-array-field' : '';
570
+ // All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
571
+ const childFieldPathId = props.childFieldPathId ?? fieldPathId;
572
+ const optionalDataControl = renderOptionalField ? (
573
+ <OptionalDataControlsField {...props} fieldPathId={childFieldPathId} />
574
+ ) : undefined;
575
+ const arrayProps: ArrayFieldTemplateProps<T[], S, F> = {
576
+ canAdd,
577
+ items: actualFormData.map((keyedItem, index: number) => {
578
+ const { key, item } = keyedItem;
579
+ // While we are actually dealing with a single item of type T, the types require a T[], so cast
580
+ const itemCast = item as unknown as T[];
581
+ const itemSchema = schemaUtils.retrieveSchema(_schemaItems, itemCast);
582
+ const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
583
+ const itemFieldPathId = toFieldPathId(index, globalFormOptions, childFieldPathId);
584
+
585
+ // Compute the item UI schema using the helper method
586
+ const itemUiSchema = computeItemUiSchema<T, S, F>(uiSchema, item, index, formContext);
587
+
588
+ const itemProps = {
589
+ itemKey: key,
590
+ index,
591
+ name: name && `${name}-${index}`,
592
+ registry,
593
+ uiOptions,
594
+ hideError,
595
+ readonly,
596
+ disabled,
597
+ required,
598
+ title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined,
599
+ canAdd,
600
+ canMoveUp: index > 0,
601
+ canMoveDown: index < formData.length - 1,
602
+ itemSchema,
603
+ itemFieldPathId,
604
+ itemErrorSchema,
605
+ itemData: itemCast,
606
+ itemUiSchema,
607
+ autofocus: autofocus && index === 0,
608
+ onBlur,
609
+ onFocus,
610
+ rawErrors,
611
+ totalItems: keyedFormData.length,
612
+ handleAddItem,
613
+ handleCopyItem,
614
+ handleRemoveItem,
615
+ handleReorderItems,
616
+ onChange,
617
+ };
618
+ return <ArrayFieldItem key={key} {...itemProps} />;
619
+ }),
620
+ className: `rjsf-field rjsf-field-array rjsf-field-array-of-${itemsSchema.type}${extraClass}`,
621
+ disabled,
622
+ fieldPathId,
623
+ uiSchema,
624
+ onAddClick: handleAddItem,
625
+ readonly,
626
+ required,
627
+ schema,
628
+ title: fieldTitle,
629
+ formData,
630
+ rawErrors,
631
+ registry,
632
+ optionalDataControl,
633
+ };
634
+
635
+ const Template = getTemplate<'ArrayFieldTemplate', T[], S, F>('ArrayFieldTemplate', registry, uiOptions);
636
+ return <Template {...arrayProps} />;
637
+ }
638
+
639
+ /** Renders an array that has a maximum limit of items
640
+ */
641
+ function FixedArray<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
642
+ props: InternalArrayFieldProps<T, S, F>,
643
+ ) {
644
+ const {
645
+ schema,
646
+ uiSchema = {},
647
+ formData,
648
+ errorSchema,
649
+ fieldPathId,
650
+ name,
651
+ title,
652
+ disabled = false,
653
+ readonly = false,
654
+ autofocus = false,
655
+ required = false,
656
+ hideError = false,
657
+ registry,
658
+ onBlur,
659
+ onFocus,
660
+ rawErrors,
661
+ keyedFormData,
662
+ onChange,
663
+ handleAddItem,
664
+ handleCopyItem,
665
+ handleRemoveItem,
666
+ handleReorderItems,
667
+ } = props;
668
+ let { formData: items = [] } = props;
669
+ const fieldTitle = schema.title || title || name;
670
+ const { schemaUtils, fields, formContext, globalFormOptions, globalUiOptions } = registry;
671
+ const uiOptions = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
672
+ const { OptionalDataControlsField } = fields;
673
+ const renderOptionalField = shouldRenderOptionalField<T[], S, F>(registry, schema, required, uiSchema);
674
+ const hasFormData = isFormDataAvailable<T[]>(formData);
675
+ const _schemaItems: S[] = isObject(schema.items) ? (schema.items as S[]) : ([] as S[]);
676
+ const itemSchemas = _schemaItems.map((item: S, index: number) =>
677
+ schemaUtils.retrieveSchema(item, items[index] as unknown as T[]),
678
+ );
679
+ const additionalSchema = isObject(schema.additionalItems)
680
+ ? schemaUtils.retrieveSchema(schema.additionalItems as S, formData)
681
+ : null;
682
+ // All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
683
+ const childFieldPathId = props.childFieldPathId ?? fieldPathId;
684
+
685
+ if (items.length < itemSchemas.length) {
686
+ // to make sure at least all fixed items are generated
687
+ items = items.concat(new Array(itemSchemas.length - items.length));
688
+ }
689
+ const actualFormData = hasFormData ? keyedFormData : [];
690
+ const extraClass = renderOptionalField ? ' rjsf-optional-array-field' : '';
691
+ const optionalDataControl = renderOptionalField ? (
692
+ <OptionalDataControlsField {...props} fieldPathId={childFieldPathId} />
693
+ ) : undefined;
694
+
695
+ // These are the props passed into the render function
696
+ const canAdd =
697
+ canAddItem<T, S, F>(registry, schema, items, uiSchema) &&
698
+ !!additionalSchema &&
699
+ (!renderOptionalField || hasFormData);
700
+ const arrayProps: ArrayFieldTemplateProps<T[], S, F> = {
701
+ canAdd,
702
+ className: `rjsf-field rjsf-field-array rjsf-field-array-fixed-items${extraClass}`,
703
+ disabled,
704
+ fieldPathId,
705
+ formData,
706
+ items: actualFormData.map((keyedItem, index) => {
707
+ const { key, item } = keyedItem;
708
+ // While we are actually dealing with a single item of type T, the types require a T[], so cast
709
+ const itemCast = item as unknown as T[];
710
+ const additional = index >= itemSchemas.length;
711
+ const itemSchema =
712
+ (additional && isObject(schema.additionalItems)
713
+ ? schemaUtils.retrieveSchema(schema.additionalItems as S, itemCast)
714
+ : itemSchemas[index]) || {};
715
+ const itemFieldPathId = toFieldPathId(index, globalFormOptions, childFieldPathId);
716
+ // Compute the item UI schema - handle both static and dynamic cases
717
+ let itemUiSchema: UiSchema<T[], S, F> | undefined;
718
+ if (additional) {
719
+ // For additional items, use additionalItems uiSchema
720
+ itemUiSchema = uiSchema.additionalItems as UiSchema<T[], S, F>;
168
721
  } else {
169
- addable = true;
722
+ // For fixed items, uiSchema.items can be an array, a function, or a single object
723
+ if (Array.isArray(uiSchema.items)) {
724
+ itemUiSchema = uiSchema.items[index] as UiSchema<T[], S, F>;
725
+ } else {
726
+ // Use the helper method for function or static object cases
727
+ itemUiSchema = computeItemUiSchema<T, S, F>(uiSchema, item, index, formContext);
728
+ }
170
729
  }
171
- }
172
- return addable;
173
- }
730
+ const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
174
731
 
175
- /** Returns the default form information for an item based on the schema for that item. Deals with the possibility
176
- * that the schema is fixed and allows additional items.
177
- */
178
- _getNewFormDataRow = (): T => {
179
- const { schema, registry } = this.props;
180
- const { schemaUtils } = registry;
181
- let itemSchema = schema.items as S;
182
- if (isFixedItems(schema) && allowAdditionalItems(schema)) {
183
- itemSchema = schema.additionalItems as S;
184
- }
185
- // Cast this as a T to work around schema utils being for T[] caused by the FieldProps<T[], S, F> call on the class
186
- return schemaUtils.getDefaultFormState(itemSchema) as unknown as T;
732
+ const itemProps = {
733
+ index,
734
+ itemKey: key,
735
+ name: name && `${name}-${index}`,
736
+ registry,
737
+ uiOptions,
738
+ hideError,
739
+ readonly,
740
+ disabled,
741
+ required,
742
+ title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined,
743
+ canAdd,
744
+ canRemove: additional,
745
+ canMoveUp: index >= itemSchemas.length + 1,
746
+ canMoveDown: additional && index < items.length - 1,
747
+ itemSchema,
748
+ itemData: itemCast,
749
+ itemUiSchema,
750
+ itemFieldPathId,
751
+ itemErrorSchema,
752
+ autofocus: autofocus && index === 0,
753
+ onBlur,
754
+ onFocus,
755
+ rawErrors,
756
+ totalItems: keyedFormData.length,
757
+ onChange,
758
+ handleAddItem,
759
+ handleCopyItem,
760
+ handleRemoveItem,
761
+ handleReorderItems,
762
+ };
763
+ return <ArrayFieldItem key={key} {...itemProps} />;
764
+ }),
765
+ onAddClick: handleAddItem,
766
+ readonly,
767
+ required,
768
+ registry,
769
+ schema,
770
+ uiSchema,
771
+ title: fieldTitle,
772
+ errorSchema,
773
+ rawErrors,
774
+ optionalDataControl,
187
775
  };
188
776
 
777
+ const Template = getTemplate<'ArrayFieldTemplate', T[], S, F>('ArrayFieldTemplate', registry, uiOptions);
778
+ return <Template {...arrayProps} />;
779
+ }
780
+
781
+ interface KeyedFormDataState<T = any> {
782
+ /** The keyed form data elements */
783
+ keyedFormData: KeyedFormDataType<T>[];
784
+ /** Updates the keyed form data elements to the given value */
785
+ updateKeyedFormData: (newData: KeyedFormDataType<T>[]) => T[];
786
+ }
787
+
788
+ /** Type used for the state of the `ArrayField` component */
789
+ type ArrayFieldState<T> = {
790
+ /** The hash of the last formData passed in */
791
+ formDataHash: string;
792
+ /** The keyed form data elements */
793
+ keyedFormData: KeyedFormDataType<T>[];
794
+ };
795
+
796
+ /** A custom hook that handles the updating of the keyedFormData from an external `formData` change as well as
797
+ * internally by the `ArrayField`. If there was an external `formData` change, then the `keyedFormData` is recomputed
798
+ * in order to preserve the unique keys from the old `keyedFormData` to the new `formData`. Along with the
799
+ * `keyedFormData` this hook also returns an `updateKeyedFormData()` function for use by the `ArrayField`. The detection
800
+ * of external `formData` are handled by storing the hash of that `formData` along with the `keyedFormData` associated
801
+ * with it. The `updateKeyedFormData()` will update that hash whenever the `keyedFormData` is modified and as well as
802
+ * returning the plain `formData` from the `keyedFormData`.
803
+ */
804
+ function useKeyedFormData<T = any>(formData: T[] = []): KeyedFormDataState<T> {
805
+ const newHash = useMemo(() => hashObject(formData), [formData]);
806
+ const [state, setState] = useState<ArrayFieldState<T>>(() => ({
807
+ formDataHash: newHash,
808
+ keyedFormData: generateKeyedFormData<T>(formData),
809
+ }));
810
+
811
+ let { keyedFormData, formDataHash } = state;
812
+ if (newHash !== formDataHash) {
813
+ const nextFormData = Array.isArray(formData) ? formData : [];
814
+ const previousKeyedFormData = keyedFormData || [];
815
+ keyedFormData =
816
+ nextFormData.length === previousKeyedFormData.length
817
+ ? previousKeyedFormData.map((previousKeyedFormDatum, index) => ({
818
+ key: previousKeyedFormDatum.key,
819
+ item: nextFormData[index],
820
+ }))
821
+ : generateKeyedFormData<T>(nextFormData);
822
+ formDataHash = newHash;
823
+ setState({ formDataHash, keyedFormData });
824
+ }
825
+
826
+ const updateKeyedFormData = useCallback((newData: KeyedFormDataType<T>[]) => {
827
+ const plainFormData = keyedToPlainFormData(newData);
828
+ const newHash = hashObject(plainFormData);
829
+ setState({ formDataHash: newHash, keyedFormData: newData });
830
+ return plainFormData;
831
+ }, []);
832
+
833
+ return { keyedFormData, updateKeyedFormData };
834
+ }
835
+
836
+ /** The `ArrayField` component is used to render a field in the schema that is of type `array`. It supports both normal
837
+ * and fixed array, allowing user to add and remove elements from the array data.
838
+ */
839
+ export default function ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
840
+ props: FieldProps<T[], S, F>,
841
+ ) {
842
+ const { schema, uiSchema, errorSchema, fieldPathId, registry, formData, onChange } = props;
843
+ const { schemaUtils, translateString } = registry;
844
+ const { keyedFormData, updateKeyedFormData } = useKeyedFormData<T>(formData);
845
+ // All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
846
+ const childFieldPathId = props.childFieldPathId ?? fieldPathId;
847
+
189
848
  /** Callback handler for when the user clicks on the add or add at index buttons. Creates a new row of keyed form data
190
849
  * either at the end of the list (when index is not specified) or inserted at the `index` when it is, adding it into
191
850
  * the state, and then returning `onChange()` with the plain form data converted from the keyed data
@@ -193,67 +852,39 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
193
852
  * @param event - The event for the click
194
853
  * @param [index] - The optional index at which to add the new data
195
854
  */
196
- _handleAddClick(event: MouseEvent, index?: number) {
197
- if (event) {
198
- event.preventDefault();
199
- }
855
+ const handleAddItem = useCallback(
856
+ (event: MouseEvent, index?: number) => {
857
+ if (event) {
858
+ event.preventDefault();
859
+ }
200
860
 
201
- const { onChange, errorSchema } = this.props;
202
- const { keyedFormData } = this.state;
203
- // refs #195: revalidate to ensure properly reindexing errors
204
- let newErrorSchema: ErrorSchema<T>;
205
- if (errorSchema) {
206
- newErrorSchema = {};
207
- for (const idx in errorSchema) {
208
- const i = parseInt(idx);
209
- if (index === undefined || i < index) {
210
- set(newErrorSchema, [i], errorSchema[idx]);
211
- } else if (i >= index) {
212
- set(newErrorSchema, [i + 1], errorSchema[idx]);
861
+ let newErrorSchema: ErrorSchema<T> | undefined;
862
+ if (errorSchema) {
863
+ newErrorSchema = {};
864
+ for (const idx in errorSchema) {
865
+ const i = parseInt(idx);
866
+ if (index === undefined || i < index) {
867
+ set(newErrorSchema, [i], errorSchema[idx]);
868
+ } else if (i >= index) {
869
+ set(newErrorSchema, [i + 1], errorSchema[idx]);
870
+ }
213
871
  }
214
872
  }
215
- }
216
-
217
- const newKeyedFormDataRow: KeyedFormDataType<T> = {
218
- key: generateRowId(),
219
- item: this._getNewFormDataRow(),
220
- };
221
- const newKeyedFormData = [...keyedFormData];
222
- if (index !== undefined) {
223
- newKeyedFormData.splice(index, 0, newKeyedFormDataRow);
224
- } else {
225
- newKeyedFormData.push(newKeyedFormDataRow);
226
- }
227
- this.setState(
228
- {
229
- keyedFormData: newKeyedFormData,
230
- updatedKeyedFormData: true,
231
- },
232
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
233
- );
234
- }
235
873
 
236
- /** Callback handler for when the user clicks on the add button. Creates a new row of keyed form data at the end of
237
- * the list, adding it into the state, and then returning `onChange()` with the plain form data converted from the
238
- * keyed data
239
- *
240
- * @param event - The event for the click
241
- */
242
- onAddClick = (event: MouseEvent) => {
243
- this._handleAddClick(event);
244
- };
245
-
246
- /** Callback handler for when the user clicks on the add button on an existing array element. Creates a new row of
247
- * keyed form data inserted at the `index`, adding it into the state, and then returning `onChange()` with the plain
248
- * form data converted from the keyed data
249
- *
250
- * @param index - The index at which the add button is clicked
251
- */
252
- onAddIndexClick = (index: number) => {
253
- return (event: MouseEvent) => {
254
- this._handleAddClick(event, index);
255
- };
256
- };
874
+ const newKeyedFormDataRow: KeyedFormDataType<T> = {
875
+ key: generateRowId(),
876
+ item: getNewFormDataRow<T, S, F>(registry, schema),
877
+ };
878
+ const newKeyedFormData = [...keyedFormData];
879
+ if (index !== undefined) {
880
+ newKeyedFormData.splice(index, 0, newKeyedFormDataRow);
881
+ } else {
882
+ newKeyedFormData.push(newKeyedFormDataRow);
883
+ }
884
+ onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema as ErrorSchema<T[]>);
885
+ },
886
+ [keyedFormData, registry, schema, onChange, updateKeyedFormData, errorSchema, childFieldPathId],
887
+ );
257
888
 
258
889
  /** Callback handler for when the user clicks on the copy button on an existing array element. Clones the row of
259
890
  * keyed form data at the `index` into the next position in the state, and then returning `onChange()` with the plain
@@ -261,16 +892,13 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
261
892
  *
262
893
  * @param index - The index at which the copy button is clicked
263
894
  */
264
- onCopyIndexClick = (index: number) => {
265
- return (event: MouseEvent) => {
895
+ const handleCopyItem = useCallback(
896
+ (event: MouseEvent, index: number) => {
266
897
  if (event) {
267
898
  event.preventDefault();
268
899
  }
269
900
 
270
- const { onChange, errorSchema } = this.props;
271
- const { keyedFormData } = this.state;
272
- // refs #195: revalidate to ensure properly reindexing errors
273
- let newErrorSchema: ErrorSchema<T>;
901
+ let newErrorSchema: ErrorSchema<T> | undefined;
274
902
  if (errorSchema) {
275
903
  newErrorSchema = {};
276
904
  for (const idx in errorSchema) {
@@ -293,15 +921,10 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
293
921
  } else {
294
922
  newKeyedFormData.push(newKeyedFormDataRow);
295
923
  }
296
- this.setState(
297
- {
298
- keyedFormData: newKeyedFormData,
299
- updatedKeyedFormData: true,
300
- },
301
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
302
- );
303
- };
304
- };
924
+ onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema as ErrorSchema<T[]>);
925
+ },
926
+ [keyedFormData, onChange, updateKeyedFormData, errorSchema, childFieldPathId],
927
+ );
305
928
 
306
929
  /** Callback handler for when the user clicks on the remove button on an existing array element. Removes the row of
307
930
  * keyed form data at the `index` in the state, and then returning `onChange()` with the plain form data converted
@@ -309,15 +932,13 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
309
932
  *
310
933
  * @param index - The index at which the remove button is clicked
311
934
  */
312
- onDropIndexClick = (index: number) => {
313
- return (event: MouseEvent) => {
935
+ const handleRemoveItem = useCallback(
936
+ (event: MouseEvent, index: number) => {
314
937
  if (event) {
315
938
  event.preventDefault();
316
939
  }
317
- const { onChange, errorSchema } = this.props;
318
- const { keyedFormData } = this.state;
319
940
  // refs #195: revalidate to ensure properly reindexing errors
320
- let newErrorSchema: ErrorSchema<T>;
941
+ let newErrorSchema: ErrorSchema<T> | undefined;
321
942
  if (errorSchema) {
322
943
  newErrorSchema = {};
323
944
  for (const idx in errorSchema) {
@@ -330,15 +951,10 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
330
951
  }
331
952
  }
332
953
  const newKeyedFormData = keyedFormData.filter((_, i) => i !== index);
333
- this.setState(
334
- {
335
- keyedFormData: newKeyedFormData,
336
- updatedKeyedFormData: true,
337
- },
338
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
339
- );
340
- };
341
- };
954
+ onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema as ErrorSchema<T[]>);
955
+ },
956
+ [keyedFormData, onChange, updateKeyedFormData, errorSchema, childFieldPathId],
957
+ );
342
958
 
343
959
  /** Callback handler for when the user clicks on one of the move item buttons on an existing array element. Moves the
344
960
  * row of keyed form data at the `index` to the `newIndex` in the state, and then returning `onChange()` with the
@@ -347,14 +963,13 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
347
963
  * @param index - The index of the item to move
348
964
  * @param newIndex - The index to where the item is to be moved
349
965
  */
350
- onReorderClick = (index: number, newIndex: number) => {
351
- return (event: MouseEvent<HTMLButtonElement>) => {
966
+ const handleReorderItems = useCallback(
967
+ (event: MouseEvent<HTMLButtonElement>, index: number, newIndex: number) => {
352
968
  if (event) {
353
969
  event.preventDefault();
354
970
  event.currentTarget.blur();
355
971
  }
356
- const { onChange, errorSchema } = this.props;
357
- let newErrorSchema: ErrorSchema<T>;
972
+ let newErrorSchema: ErrorSchema<T> | undefined;
358
973
  if (errorSchema) {
359
974
  newErrorSchema = {};
360
975
  for (const idx in errorSchema) {
@@ -369,7 +984,6 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
369
984
  }
370
985
  }
371
986
 
372
- const { keyedFormData } = this.state;
373
987
  function reOrderArray() {
374
988
  // Copy item
375
989
  const _newKeyedFormData = keyedFormData.slice();
@@ -381,541 +995,75 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
381
995
  return _newKeyedFormData;
382
996
  }
383
997
  const newKeyedFormData = reOrderArray();
384
- this.setState(
385
- {
386
- keyedFormData: newKeyedFormData,
387
- },
388
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
389
- );
390
- };
391
- };
998
+ onChange(updateKeyedFormData(newKeyedFormData), childFieldPathId.path, newErrorSchema as ErrorSchema<T[]>);
999
+ },
1000
+ [keyedFormData, onChange, updateKeyedFormData, errorSchema, childFieldPathId],
1001
+ );
392
1002
 
393
1003
  /** Callback handler used to deal with changing the value of the data in the array at the `index`. Calls the
394
1004
  * `onChange` callback with the updated form data
395
1005
  *
396
1006
  * @param index - The index of the item being changed
397
1007
  */
398
- onChangeForIndex = (index: number) => {
399
- return (value: any, newErrorSchema?: ErrorSchema<T>, id?: string) => {
400
- const { formData, onChange, errorSchema } = this.props;
401
- const arrayData = Array.isArray(formData) ? formData : [];
402
- const newFormData = arrayData.map((item: T, i: number) => {
1008
+ const handleChange = useCallback(
1009
+ (value: any, path: FieldPathList, newErrorSchema?: ErrorSchema<T>, id?: string) => {
1010
+ onChange(
403
1011
  // We need to treat undefined items as nulls to have validation.
404
1012
  // See https://github.com/tdegrunt/jsonschema/issues/206
405
- const jsonValue = typeof value === 'undefined' ? null : value;
406
- return index === i ? jsonValue : item;
407
- });
408
- onChange(
409
- newFormData,
410
- errorSchema &&
411
- errorSchema && {
412
- ...errorSchema,
413
- [index]: newErrorSchema,
414
- },
1013
+ value === undefined ? null : value,
1014
+ path,
1015
+ newErrorSchema as ErrorSchema<T[]>,
415
1016
  id,
416
1017
  );
417
- };
418
- };
1018
+ },
1019
+ [onChange],
1020
+ );
419
1021
 
420
1022
  /** Callback handler used to change the value for a checkbox */
421
- onSelectChange = (value: any) => {
422
- const { onChange, idSchema } = this.props;
423
- onChange(value, undefined, idSchema && idSchema.$id);
424
- };
425
-
426
- /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements
427
- */
428
- render() {
429
- const { schema, uiSchema, idSchema, registry } = this.props;
430
- const { schemaUtils, translateString } = registry;
431
- if (!(ITEMS_KEY in schema)) {
432
- const uiOptions = getUiOptions<T[], S, F>(uiSchema);
433
- const UnsupportedFieldTemplate = getTemplate<'UnsupportedFieldTemplate', T[], S, F>(
434
- 'UnsupportedFieldTemplate',
435
- registry,
436
- uiOptions,
437
- );
1023
+ const onSelectChange = useCallback(
1024
+ (value: any) => {
1025
+ onChange(value, childFieldPathId.path, undefined, childFieldPathId?.[ID_KEY]);
1026
+ },
1027
+ [onChange, childFieldPathId],
1028
+ );
438
1029
 
439
- return (
440
- <UnsupportedFieldTemplate
441
- schema={schema}
442
- idSchema={idSchema}
443
- reason={translateString(TranslatableString.MissingItems)}
444
- registry={registry}
445
- />
446
- );
447
- }
448
- if (schemaUtils.isMultiSelect(schema)) {
449
- // If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
450
- return this.renderMultiSelect();
451
- }
452
- if (isCustomWidget<T[], S, F>(uiSchema)) {
453
- return this.renderCustomWidget();
454
- }
455
- if (isFixedItems(schema)) {
456
- return this.renderFixedArray();
457
- }
458
- if (schemaUtils.isFilesArray(schema, uiSchema)) {
459
- return this.renderFiles();
460
- }
461
- return this.renderNormalArray();
462
- }
463
-
464
- /** Renders a normal array without any limitations of length
465
- */
466
- renderNormalArray() {
467
- const {
468
- schema,
469
- uiSchema = {},
470
- errorSchema,
471
- idSchema,
472
- name,
473
- title,
474
- disabled = false,
475
- readonly = false,
476
- autofocus = false,
477
- required = false,
478
- registry,
479
- onBlur,
480
- onFocus,
481
- idPrefix,
482
- idSeparator = '_',
483
- rawErrors,
484
- } = this.props;
485
- const { keyedFormData } = this.state;
486
- const fieldTitle = schema.title || title || name;
487
- const { schemaUtils, formContext } = registry;
1030
+ if (!(ITEMS_KEY in schema)) {
488
1031
  const uiOptions = getUiOptions<T[], S, F>(uiSchema);
489
- const _schemaItems: S = isObject(schema.items) ? (schema.items as S) : ({} as S);
490
- const itemsSchema: S = schemaUtils.retrieveSchema(_schemaItems);
491
- const formData = keyedToPlainFormData(this.state.keyedFormData);
492
- const canAdd = this.canAddItem(formData);
493
- const arrayProps: ArrayFieldTemplateProps<T[], S, F> = {
494
- canAdd,
495
- items: keyedFormData.map((keyedItem, index) => {
496
- const { key, item } = keyedItem;
497
- // While we are actually dealing with a single item of type T, the types require a T[], so cast
498
- const itemCast = item as unknown as T[];
499
- const itemSchema = schemaUtils.retrieveSchema(_schemaItems, itemCast);
500
- const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
501
- const itemIdPrefix = idSchema.$id + idSeparator + index;
502
- const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
503
- return this.renderArrayFieldItem({
504
- key,
505
- index,
506
- name: name && `${name}-${index}`,
507
- title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined,
508
- canAdd,
509
- canMoveUp: index > 0,
510
- canMoveDown: index < formData.length - 1,
511
- itemSchema,
512
- itemIdSchema,
513
- itemErrorSchema,
514
- itemData: itemCast,
515
- itemUiSchema: uiSchema.items,
516
- autofocus: autofocus && index === 0,
517
- onBlur,
518
- onFocus,
519
- rawErrors,
520
- totalItems: keyedFormData.length,
521
- });
522
- }),
523
- className: `rjsf-field rjsf-field-array rjsf-field-array-of-${itemsSchema.type}`,
524
- disabled,
525
- idSchema,
526
- uiSchema,
527
- onAddClick: this.onAddClick,
528
- readonly,
529
- required,
530
- schema,
531
- title: fieldTitle,
532
- formContext,
533
- formData,
534
- rawErrors,
1032
+ const UnsupportedFieldTemplate = getTemplate<'UnsupportedFieldTemplate', T[], S, F>(
1033
+ 'UnsupportedFieldTemplate',
535
1034
  registry,
536
- };
537
-
538
- const Template = getTemplate<'ArrayFieldTemplate', T[], S, F>('ArrayFieldTemplate', registry, uiOptions);
539
- return <Template {...arrayProps} />;
540
- }
541
-
542
- /** Renders an array using the custom widget provided by the user in the `uiSchema`
543
- */
544
- renderCustomWidget() {
545
- const {
546
- schema,
547
- idSchema,
548
- uiSchema,
549
- disabled = false,
550
- readonly = false,
551
- autofocus = false,
552
- required = false,
553
- hideError,
554
- placeholder,
555
- onBlur,
556
- onFocus,
557
- formData: items = [],
558
- registry,
559
- rawErrors,
560
- name,
561
- } = this.props;
562
- const { widgets, formContext, globalUiOptions, schemaUtils } = registry;
563
- const { widget, title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
564
- const Widget = getWidget<T[], S, F>(schema, widget, widgets);
565
- const label = uiTitle ?? schema.title ?? name;
566
- const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
567
- return (
568
- <Widget
569
- id={idSchema.$id}
570
- name={name}
571
- multiple
572
- onChange={this.onSelectChange}
573
- onBlur={onBlur}
574
- onFocus={onFocus}
575
- options={options}
576
- schema={schema}
577
- uiSchema={uiSchema}
578
- registry={registry}
579
- value={items}
580
- disabled={disabled}
581
- readonly={readonly}
582
- hideError={hideError}
583
- required={required}
584
- label={label}
585
- hideLabel={!displayLabel}
586
- placeholder={placeholder}
587
- formContext={formContext}
588
- autofocus={autofocus}
589
- rawErrors={rawErrors}
590
- />
1035
+ uiOptions,
591
1036
  );
592
- }
593
1037
 
594
- /** Renders an array as a set of checkboxes
595
- */
596
- renderMultiSelect() {
597
- const {
598
- schema,
599
- idSchema,
600
- uiSchema,
601
- formData: items = [],
602
- disabled = false,
603
- readonly = false,
604
- autofocus = false,
605
- required = false,
606
- placeholder,
607
- onBlur,
608
- onFocus,
609
- registry,
610
- rawErrors,
611
- name,
612
- } = this.props;
613
- const { widgets, schemaUtils, formContext, globalUiOptions } = registry;
614
- const itemsSchema = schemaUtils.retrieveSchema(schema.items as S, items);
615
- const enumOptions = optionsList<T[], S, F>(itemsSchema, uiSchema);
616
- const { widget = 'select', title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
617
- const Widget = getWidget<T[], S, F>(schema, widget, widgets);
618
- const label = uiTitle ?? schema.title ?? name;
619
- const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
620
1038
  return (
621
- <Widget
622
- id={idSchema.$id}
623
- name={name}
624
- multiple
625
- onChange={this.onSelectChange}
626
- onBlur={onBlur}
627
- onFocus={onFocus}
628
- options={{ ...options, enumOptions }}
1039
+ <UnsupportedFieldTemplate
629
1040
  schema={schema}
630
- uiSchema={uiSchema}
1041
+ fieldPathId={fieldPathId}
1042
+ reason={translateString(TranslatableString.MissingItems)}
631
1043
  registry={registry}
632
- value={items}
633
- disabled={disabled}
634
- readonly={readonly}
635
- required={required}
636
- label={label}
637
- hideLabel={!displayLabel}
638
- placeholder={placeholder}
639
- formContext={formContext}
640
- autofocus={autofocus}
641
- rawErrors={rawErrors}
642
1044
  />
643
1045
  );
644
1046
  }
645
-
646
- /** Renders an array of files using the `FileWidget`
647
- */
648
- renderFiles() {
649
- const {
650
- schema,
651
- uiSchema,
652
- idSchema,
653
- name,
654
- disabled = false,
655
- readonly = false,
656
- autofocus = false,
657
- required = false,
658
- onBlur,
659
- onFocus,
660
- registry,
661
- formData: items = [],
662
- rawErrors,
663
- } = this.props;
664
- const { widgets, formContext, globalUiOptions, schemaUtils } = registry;
665
- const { widget = 'files', title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
666
- const Widget = getWidget<T[], S, F>(schema, widget, widgets);
667
- const label = uiTitle ?? schema.title ?? name;
668
- const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
669
- return (
670
- <Widget
671
- options={options}
672
- id={idSchema.$id}
673
- name={name}
674
- multiple
675
- onChange={this.onSelectChange}
676
- onBlur={onBlur}
677
- onFocus={onFocus}
678
- schema={schema}
679
- uiSchema={uiSchema}
680
- value={items}
681
- disabled={disabled}
682
- readonly={readonly}
683
- required={required}
684
- registry={registry}
685
- formContext={formContext}
686
- autofocus={autofocus}
687
- rawErrors={rawErrors}
688
- label={label}
689
- hideLabel={!displayLabel}
690
- />
691
- );
1047
+ const arrayProps = {
1048
+ handleAddItem,
1049
+ handleCopyItem,
1050
+ handleRemoveItem,
1051
+ handleReorderItems,
1052
+ keyedFormData,
1053
+ onChange: handleChange,
1054
+ };
1055
+ if (schemaUtils.isMultiSelect(schema)) {
1056
+ // If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
1057
+ return <ArrayAsMultiSelect<T, S, F> {...props} fieldPathId={childFieldPathId} onSelectChange={onSelectChange} />;
692
1058
  }
693
-
694
- /** Renders an array that has a maximum limit of items
695
- */
696
- renderFixedArray() {
697
- const {
698
- schema,
699
- uiSchema = {},
700
- formData = [],
701
- errorSchema,
702
- idPrefix,
703
- idSeparator = '_',
704
- idSchema,
705
- name,
706
- title,
707
- disabled = false,
708
- readonly = false,
709
- autofocus = false,
710
- required = false,
711
- registry,
712
- onBlur,
713
- onFocus,
714
- rawErrors,
715
- } = this.props;
716
- const { keyedFormData } = this.state;
717
- let { formData: items = [] } = this.props;
718
- const fieldTitle = schema.title || title || name;
719
- const uiOptions = getUiOptions<T[], S, F>(uiSchema);
720
- const { schemaUtils, formContext } = registry;
721
- const _schemaItems: S[] = isObject(schema.items) ? (schema.items as S[]) : ([] as S[]);
722
- const itemSchemas = _schemaItems.map((item: S, index: number) =>
723
- schemaUtils.retrieveSchema(item, formData[index] as unknown as T[]),
724
- );
725
- const additionalSchema = isObject(schema.additionalItems)
726
- ? schemaUtils.retrieveSchema(schema.additionalItems as S, formData)
727
- : null;
728
-
729
- if (!items || items.length < itemSchemas.length) {
730
- // to make sure at least all fixed items are generated
731
- items = items || [];
732
- items = items.concat(new Array(itemSchemas.length - items.length));
733
- }
734
-
735
- // These are the props passed into the render function
736
- const canAdd = this.canAddItem(items) && !!additionalSchema;
737
- const arrayProps: ArrayFieldTemplateProps<T[], S, F> = {
738
- canAdd,
739
- className: 'rjsf-field rjsf-field-array rjsf-field-array-fixed-items',
740
- disabled,
741
- idSchema,
742
- formData,
743
- items: keyedFormData.map((keyedItem, index) => {
744
- const { key, item } = keyedItem;
745
- // While we are actually dealing with a single item of type T, the types require a T[], so cast
746
- const itemCast = item as unknown as T[];
747
- const additional = index >= itemSchemas.length;
748
- const itemSchema =
749
- (additional && isObject(schema.additionalItems)
750
- ? schemaUtils.retrieveSchema(schema.additionalItems as S, itemCast)
751
- : itemSchemas[index]) || {};
752
- const itemIdPrefix = idSchema.$id + idSeparator + index;
753
- const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
754
- const itemUiSchema = additional
755
- ? uiSchema.additionalItems || {}
756
- : Array.isArray(uiSchema.items)
757
- ? uiSchema.items[index]
758
- : uiSchema.items || {};
759
- const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
760
-
761
- return this.renderArrayFieldItem({
762
- key,
763
- index,
764
- name: name && `${name}-${index}`,
765
- title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined,
766
- canAdd,
767
- canRemove: additional,
768
- canMoveUp: index >= itemSchemas.length + 1,
769
- canMoveDown: additional && index < items.length - 1,
770
- itemSchema,
771
- itemData: itemCast,
772
- itemUiSchema,
773
- itemIdSchema,
774
- itemErrorSchema,
775
- autofocus: autofocus && index === 0,
776
- onBlur,
777
- onFocus,
778
- rawErrors,
779
- totalItems: keyedFormData.length,
780
- });
781
- }),
782
- onAddClick: this.onAddClick,
783
- readonly,
784
- required,
785
- registry,
786
- schema,
787
- uiSchema,
788
- title: fieldTitle,
789
- formContext,
790
- errorSchema,
791
- rawErrors,
792
- };
793
-
794
- const Template = getTemplate<'ArrayFieldTemplate', T[], S, F>('ArrayFieldTemplate', registry, uiOptions);
795
- return <Template {...arrayProps} />;
1059
+ if (isCustomWidget<T[], S, F>(uiSchema)) {
1060
+ return <ArrayAsCustomWidget<T, S, F> {...props} fieldPathId={childFieldPathId} onSelectChange={onSelectChange} />;
796
1061
  }
797
-
798
- /** Renders the individual array item using a `SchemaField` along with the additional properties required to be send
799
- * back to the `ArrayFieldItemTemplate`.
800
- *
801
- * @param props - The props for the individual array item to be rendered
802
- */
803
- renderArrayFieldItem(props: {
804
- key: string;
805
- index: number;
806
- name: string;
807
- title: string | undefined;
808
- canAdd: boolean;
809
- canRemove?: boolean;
810
- canMoveUp: boolean;
811
- canMoveDown: boolean;
812
- itemSchema: S;
813
- itemData: T[];
814
- itemUiSchema: UiSchema<T[], S, F>;
815
- itemIdSchema: IdSchema<T[]>;
816
- itemErrorSchema?: ErrorSchema<T[]>;
817
- autofocus?: boolean;
818
- onBlur: FieldProps<T[], S, F>['onBlur'];
819
- onFocus: FieldProps<T[], S, F>['onFocus'];
820
- rawErrors?: string[];
821
- totalItems: number;
822
- }) {
823
- const {
824
- key,
825
- index,
826
- name,
827
- canAdd,
828
- canRemove = true,
829
- canMoveUp,
830
- canMoveDown,
831
- itemSchema,
832
- itemData,
833
- itemUiSchema,
834
- itemIdSchema,
835
- itemErrorSchema,
836
- autofocus,
837
- onBlur,
838
- onFocus,
839
- rawErrors,
840
- totalItems,
841
- title,
842
- } = props;
843
- const { disabled, hideError, idPrefix, idSeparator, readonly, uiSchema, registry, formContext } = this.props;
844
- const {
845
- fields: { ArraySchemaField, SchemaField },
846
- globalUiOptions,
847
- } = registry;
848
- const ItemSchemaField = ArraySchemaField || SchemaField;
849
- const { orderable = true, removable = true, copyable = false } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
850
- const has: { [key: string]: boolean } = {
851
- moveUp: orderable && canMoveUp,
852
- moveDown: orderable && canMoveDown,
853
- copy: copyable && canAdd,
854
- remove: removable && canRemove,
855
- toolbar: false,
856
- };
857
- has.toolbar = Object.keys(has).some((key: keyof typeof has) => has[key]);
858
-
859
- return {
860
- children: (
861
- <ItemSchemaField
862
- name={name}
863
- title={title}
864
- index={index}
865
- schema={itemSchema}
866
- uiSchema={itemUiSchema}
867
- formData={itemData}
868
- formContext={formContext}
869
- errorSchema={itemErrorSchema}
870
- idPrefix={idPrefix}
871
- idSeparator={idSeparator}
872
- idSchema={itemIdSchema}
873
- required={this.isItemRequired(itemSchema)}
874
- onChange={this.onChangeForIndex(index)}
875
- onBlur={onBlur}
876
- onFocus={onFocus}
877
- registry={registry}
878
- disabled={disabled}
879
- readonly={readonly}
880
- hideError={hideError}
881
- autofocus={autofocus}
882
- rawErrors={rawErrors}
883
- />
884
- ),
885
- buttonsProps: {
886
- idSchema: itemIdSchema,
887
- disabled: disabled,
888
- readonly: readonly,
889
- canAdd,
890
- hasCopy: has.copy,
891
- hasMoveUp: has.moveUp,
892
- hasMoveDown: has.moveDown,
893
- hasRemove: has.remove,
894
- index: index,
895
- totalItems: totalItems,
896
- onAddIndexClick: this.onAddIndexClick,
897
- onCopyIndexClick: this.onCopyIndexClick,
898
- onDropIndexClick: this.onDropIndexClick,
899
- onReorderClick: this.onReorderClick,
900
- registry: registry,
901
- schema: itemSchema,
902
- uiSchema: itemUiSchema,
903
- },
904
- className: 'rjsf-array-item',
905
- disabled,
906
- hasToolbar: has.toolbar,
907
- index,
908
- totalItems,
909
- key,
910
- readonly,
911
- registry,
912
- schema: itemSchema,
913
- uiSchema: itemUiSchema,
914
- };
1062
+ if (isFixedItems(schema)) {
1063
+ return <FixedArray<T, S, F> {...props} {...arrayProps} />;
1064
+ }
1065
+ if (schemaUtils.isFilesArray(schema, uiSchema)) {
1066
+ return <ArrayAsFiles {...props} fieldPathId={childFieldPathId} onSelectChange={onSelectChange} />;
915
1067
  }
1068
+ return <NormalArray<T, S, F> {...props} {...arrayProps} />;
916
1069
  }
917
-
918
- /** `ArrayField` is `React.ComponentType<FieldProps<T[], S, F>>` (necessarily) but the `registry` requires things to be a
919
- * `Field` which is defined as `React.ComponentType<FieldProps<T, S, F>>`, so cast it to make `registry` happy.
920
- */
921
- export default ArrayField;