@rjsf/core 6.0.0-beta.9 → 6.0.0

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
@@ -4,13 +4,15 @@ import {
4
4
  CustomValidator,
5
5
  deepEquals,
6
6
  ErrorSchema,
7
+ ErrorSchemaBuilder,
7
8
  ErrorTransformer,
9
+ FieldPathId,
10
+ FieldPathList,
8
11
  FormContextType,
9
12
  GenericObjectType,
10
13
  getChangedFields,
11
14
  getTemplate,
12
15
  getUiOptions,
13
- IdSchema,
14
16
  isObject,
15
17
  mergeObjects,
16
18
  NAME_KEY,
@@ -27,6 +29,7 @@ import {
27
29
  SUBMIT_BTN_OPTIONS_KEY,
28
30
  TemplatesType,
29
31
  toErrorList,
32
+ toFieldPathId,
30
33
  UiSchema,
31
34
  UI_GLOBAL_OPTIONS_KEY,
32
35
  UI_OPTIONS_KEY,
@@ -35,18 +38,25 @@ import {
35
38
  ValidatorType,
36
39
  Experimental_DefaultFormStateBehavior,
37
40
  Experimental_CustomMergeAllOf,
38
- createErrorHandler,
39
- unwrapErrorHandler,
41
+ DEFAULT_ID_SEPARATOR,
42
+ DEFAULT_ID_PREFIX,
43
+ GlobalFormOptions,
44
+ ERRORS_KEY,
45
+ ID_KEY,
46
+ NameGeneratorFunction,
40
47
  } from '@rjsf/utils';
41
- import _forEach from 'lodash/forEach';
48
+ import _cloneDeep from 'lodash/cloneDeep';
42
49
  import _get from 'lodash/get';
43
50
  import _isEmpty from 'lodash/isEmpty';
44
- import _isNil from 'lodash/isNil';
45
51
  import _pick from 'lodash/pick';
52
+ import _set from 'lodash/set';
46
53
  import _toPath from 'lodash/toPath';
47
54
 
48
55
  import getDefaultRegistry from '../getDefaultRegistry';
49
56
 
57
+ /** Internal only symbol used by the `reset()` function to indicate that a reset operation is happening */
58
+ const IS_RESET = Symbol('reset');
59
+
50
60
  /** The properties that are passed to the `Form` */
51
61
  export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> {
52
62
  /** The JSON schema object for the form */
@@ -57,8 +67,14 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
57
67
  children?: ReactNode;
58
68
  /** The uiSchema for the form */
59
69
  uiSchema?: UiSchema<T, S, F>;
60
- /** The data for the form, used to prefill a form with existing data */
70
+ /** The data for the form, used to load a "controlled" form with its current data. If you want an "uncontrolled" form
71
+ * with initial data, then use `initialFormData` instead.
72
+ */
61
73
  formData?: T;
74
+ /** The initial data for the form, used to fill an "uncontrolled" form with existing data on the initial render and
75
+ * when `reset()` is called programmatically.
76
+ */
77
+ initialFormData?: T;
62
78
  // Form presentation and behavior modifiers
63
79
  /** You can provide a `formContext` object to the form, which is passed down to all fields and widgets. Useful for
64
80
  * implementing context aware fields and widgets.
@@ -161,14 +177,28 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
161
177
  * @deprecated - In a future release, this switch may be replaced by making `validator` prop optional
162
178
  */
163
179
  noValidate?: boolean;
164
- /** If set to true, the form will perform validation and show any validation errors whenever the form data is changed,
165
- * rather than just on submit
180
+ /** Flag that describes when live validation will be performed. Live validation means that the form will perform
181
+ * validation and show any validation errors whenever the form data is updated, rather than just on submit.
182
+ *
183
+ * If no value (or `false`) is provided, then live validation will not happen. If `true` or `onChange` is provided for
184
+ * the flag, then live validation will be performed after processing of all pending changes has completed. If `onBlur`
185
+ * is provided, then live validation will be performed when a field that was updated is blurred (as a performance
186
+ * optimization).
187
+ *
188
+ * @deprecated - In a future major release, the `boolean` options for this flag will be removed
166
189
  */
167
- liveValidate?: boolean;
168
- /** If `omitExtraData` and `liveOmit` are both set to true, then extra form data values that are not in any form field
169
- * will be removed whenever `onChange` is called. Set to `false` by default
190
+ liveValidate?: boolean | 'onChange' | 'onBlur';
191
+ /** Flag that describes when live omit will be performed. Live omit happens only when `omitExtraData` is also set to
192
+ * to `true` and the form's data is updated by the user.
193
+ *
194
+ * If no value (or `false`) is provided, then live omit will not happen. If `true` or `onChange` is provided for
195
+ * the flag, then live omit will be performed after processing of all pending changes has completed. If `onBlur`
196
+ * is provided, then live omit will be performed when a field that was updated is blurred (as a performance
197
+ * optimization).
198
+ *
199
+ * @deprecated - In a future major release, the `boolean` options for this flag will be removed
170
200
  */
171
- liveOmit?: boolean;
201
+ liveOmit?: boolean | 'onChange' | 'onBlur';
172
202
  /** If set to true, then extra form data values that are not in any form field will be removed whenever `onSubmit` is
173
203
  * called. Set to `false` by default.
174
204
  */
@@ -190,11 +220,29 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
190
220
  * to put the second parameter before the first in its translation.
191
221
  */
192
222
  translateString?: Registry['translateString'];
223
+ /** Optional function to generate custom HTML `name` attributes for form fields.
224
+ */
225
+ nameGenerator?: NameGeneratorFunction;
226
+ /** Optional flag that, when set to true, will cause the `FallbackField` to render a type selector for unsupported
227
+ * fields instead of the default UnsupportedField error UI.
228
+ */
229
+ useFallbackUiForUnsupportedType?: boolean;
193
230
  /** Optional configuration object with flags, if provided, allows users to override default form state behavior
194
231
  * Currently only affecting minItems on array fields and handling of setting defaults based on the value of
195
232
  * `emptyObjectFields`
196
233
  */
197
234
  experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
235
+ /**
236
+ * Controls the component update strategy used by the Form's `shouldComponentUpdate` lifecycle method.
237
+ *
238
+ * - `'customDeep'`: Uses RJSF's custom deep equality checks via the `deepEquals` utility function,
239
+ * which treats all functions as equivalent and provides optimized performance for form data comparisons.
240
+ * - `'shallow'`: Uses shallow comparison of props and state (only compares direct properties). This matches React's PureComponent behavior.
241
+ * - `'always'`: Always rerenders when called. This matches React's Component behavior.
242
+ *
243
+ * @default 'customDeep'
244
+ */
245
+ experimental_componentUpdateStrategy?: 'customDeep' | 'shallow' | 'always';
198
246
  /** Optional function that allows for custom merging of `allOf` schemas
199
247
  */
200
248
  experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
@@ -226,10 +274,10 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
226
274
  schema: S;
227
275
  /** The uiSchema for the form */
228
276
  uiSchema: UiSchema<T, S, F>;
229
- /** The `IdSchema` for the form, computed from the `schema`, the `rootFieldId`, the `formData` and the `idPrefix` and
277
+ /** The `FieldPathId` for the form, computed from the `schema`, the `rootFieldId`, the `idPrefix` and
230
278
  * `idSeparator` props.
231
279
  */
232
- idSchema: IdSchema<T>;
280
+ fieldPathId: FieldPathId;
233
281
  /** The schemaUtils implementation used by the `Form`, created from the `validator` and the `schema` */
234
282
  schemaUtils: SchemaUtilsType<T, S, F>;
235
283
  /** The current data for the form, computed from the `formData` prop and the changes made by the user */
@@ -240,26 +288,64 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
240
288
  errors: RJSFValidationError[];
241
289
  /** The current errors, in `ErrorSchema` format, for the form, includes `extraErrors` */
242
290
  errorSchema: ErrorSchema<T>;
291
+ // Private
243
292
  /** The current list of errors for the form directly from schema validation, does NOT include `extraErrors` */
244
293
  schemaValidationErrors: RJSFValidationError[];
245
294
  /** The current errors, in `ErrorSchema` format, for the form directly from schema validation, does NOT include
246
295
  * `extraErrors`
247
296
  */
248
297
  schemaValidationErrorSchema: ErrorSchema<T>;
249
- // Private
298
+ /** A container used to handle custom errors provided via `onChange` */
299
+ customErrors?: ErrorSchemaBuilder<T>;
250
300
  /** @description result of schemaUtils.retrieveSchema(schema, formData). This a memoized value to avoid re calculate at internal functions (getStateFromProps, onChange) */
251
301
  retrievedSchema: S;
302
+ /** Flag indicating whether the initial form defaults have been generated */
303
+ initialDefaultsGenerated: boolean;
304
+ /** The registry (re)computed only when props changed */
305
+ registry: Registry<T, S, F>;
252
306
  }
253
307
 
254
308
  /** The event data passed when changes have been made to the form, includes everything from the `FormState` except
255
309
  * the schema validation errors. An additional `status` is added when returned from `onSubmit`
256
310
  */
257
311
  export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
258
- extends Omit<FormState<T, S, F>, 'schemaValidationErrors' | 'schemaValidationErrorSchema'> {
312
+ extends Pick<
313
+ FormState<T, S, F>,
314
+ 'schema' | 'uiSchema' | 'fieldPathId' | 'schemaUtils' | 'formData' | 'edit' | 'errors' | 'errorSchema'
315
+ > {
259
316
  /** The status of the form when submitted */
260
317
  status?: 'submitted';
261
318
  }
262
319
 
320
+ /** Converts the full `FormState` into the `IChangeEvent` version by picking out the public values
321
+ *
322
+ * @param state - The state of the form
323
+ * @param status - The status provided by the onSubmit
324
+ * @returns - The `IChangeEvent` for the state
325
+ */
326
+ function toIChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
327
+ state: FormState<T, S, F>,
328
+ status?: IChangeEvent['status'],
329
+ ): IChangeEvent<T, S, F> {
330
+ return {
331
+ ..._pick(state, ['schema', 'uiSchema', 'fieldPathId', 'schemaUtils', 'formData', 'edit', 'errors', 'errorSchema']),
332
+ ...(status !== undefined && { status }),
333
+ };
334
+ }
335
+
336
+ /** The definition of a pending change that will be processed in the `onChange` handler
337
+ */
338
+ interface PendingChange<T> {
339
+ /** The path into the formData/errorSchema at which the `newValue`/`newErrorSchema` will be set */
340
+ path: FieldPathList;
341
+ /** The new value to set into the formData */
342
+ newValue?: T;
343
+ /** The new errors to be set into the errorSchema, if any */
344
+ newErrorSchema?: ErrorSchema<T>;
345
+ /** The optional id of the field for which the change is being made */
346
+ id?: string;
347
+ }
348
+
263
349
  /** The `Form` component renders the outer form and all the fields defined in the `schema` */
264
350
  export default class Form<
265
351
  T = any,
@@ -271,6 +357,10 @@ export default class Form<
271
357
  */
272
358
  formElement: RefObject<any>;
273
359
 
360
+ /** The list of pending changes
361
+ */
362
+ pendingChanges: PendingChange<T>[] = [];
363
+
274
364
  /** Constructs the `Form` from the `props`. Will setup the initial state from the props. It will also call the
275
365
  * `onChange` handler if the initially provided `formData` is modified to add missing default values as part of the
276
366
  * state construction.
@@ -284,9 +374,11 @@ export default class Form<
284
374
  throw new Error('A validator is required for Form functionality to work');
285
375
  }
286
376
 
287
- this.state = this.getStateFromProps(props, props.formData);
288
- if (this.props.onChange && !deepEquals(this.state.formData, this.props.formData)) {
289
- this.props.onChange(this.state);
377
+ const { formData: propsFormData, initialFormData, onChange } = props;
378
+ const formData = propsFormData ?? initialFormData;
379
+ this.state = this.getStateFromProps(props, formData, undefined, undefined, undefined, true);
380
+ if (onChange && !deepEquals(this.state.formData, formData)) {
381
+ onChange(toIChangeEvent(this.state));
290
382
  }
291
383
  this.formElement = createRef();
292
384
  }
@@ -314,12 +406,18 @@ export default class Form<
314
406
  prevState: FormState<T, S, F>,
315
407
  ): { nextState: FormState<T, S, F>; shouldUpdate: true } | { shouldUpdate: false } {
316
408
  if (!deepEquals(this.props, prevProps)) {
409
+ // Compare the previous props formData against the current props formData
317
410
  const formDataChangedFields = getChangedFields(this.props.formData, prevProps.formData);
411
+ // Compare the current props formData against the current state's formData to determine if the new props were the
412
+ // result of the onChange from the existing state formData
413
+ const stateDataChangedFields = getChangedFields(this.props.formData, this.state.formData);
318
414
  const isSchemaChanged = !deepEquals(prevProps.schema, this.props.schema);
319
415
  // When formData is not an object, getChangedFields returns an empty array.
320
416
  // In this case, deepEquals is most needed to check again.
321
417
  const isFormDataChanged =
322
418
  formDataChangedFields.length > 0 || !deepEquals(prevProps.formData, this.props.formData);
419
+ const isStateDataChanged =
420
+ stateDataChangedFields.length > 0 || !deepEquals(this.state.formData, this.props.formData);
323
421
  const nextState = this.getStateFromProps(
324
422
  this.props,
325
423
  this.props.formData,
@@ -329,6 +427,8 @@ export default class Form<
329
427
  isSchemaChanged || isFormDataChanged ? undefined : this.state.retrievedSchema,
330
428
  isSchemaChanged,
331
429
  formDataChangedFields,
430
+ // Skip live validation for this request if no form data has changed from the last state
431
+ !isStateDataChanged,
332
432
  );
333
433
  const shouldUpdate = !deepEquals(nextState, prevState);
334
434
  return { nextState, shouldUpdate };
@@ -355,13 +455,12 @@ export default class Form<
355
455
  ) {
356
456
  if (snapshot.shouldUpdate) {
357
457
  const { nextState } = snapshot;
358
-
359
458
  if (
360
459
  !deepEquals(nextState.formData, this.props.formData) &&
361
460
  !deepEquals(nextState.formData, prevState.formData) &&
362
461
  this.props.onChange
363
462
  ) {
364
- this.props.onChange(nextState);
463
+ this.props.onChange(toIChangeEvent(nextState));
365
464
  }
366
465
  this.setState(nextState);
367
466
  }
@@ -376,6 +475,7 @@ export default class Form<
376
475
  * @param retrievedSchema - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`.
377
476
  * @param isSchemaChanged - A flag indicating whether the schema has changed.
378
477
  * @param formDataChangedFields - The changed fields of `formData`
478
+ * @param skipLiveValidate - Optional flag, if true, means that we are not running live validation
379
479
  * @returns - The new state for the `Form`
380
480
  */
381
481
  getStateFromProps(
@@ -384,14 +484,16 @@ export default class Form<
384
484
  retrievedSchema?: S,
385
485
  isSchemaChanged = false,
386
486
  formDataChangedFields: string[] = [],
487
+ skipLiveValidate = false,
387
488
  ): FormState<T, S, F> {
388
489
  const state: FormState<T, S, F> = this.state || {};
389
490
  const schema = 'schema' in props ? props.schema : this.props.schema;
491
+ const validator = 'validator' in props ? props.validator : this.props.validator;
390
492
  const uiSchema: UiSchema<T, S, F> = ('uiSchema' in props ? props.uiSchema! : this.props.uiSchema!) || {};
493
+ const isUncontrolled = props.formData === undefined && this.props.formData === undefined;
391
494
  const edit = typeof inputFormData !== 'undefined';
392
495
  const liveValidate = 'liveValidate' in props ? props.liveValidate : this.props.liveValidate;
393
496
  const mustValidate = edit && !props.noValidate && liveValidate;
394
- const rootSchema = schema;
395
497
  const experimental_defaultFormStateBehavior =
396
498
  'experimental_defaultFormStateBehavior' in props
397
499
  ? props.experimental_defaultFormStateBehavior
@@ -404,22 +506,37 @@ export default class Form<
404
506
  if (
405
507
  !schemaUtils ||
406
508
  schemaUtils.doesSchemaUtilsDiffer(
407
- props.validator,
408
- rootSchema,
509
+ validator,
510
+ schema,
409
511
  experimental_defaultFormStateBehavior,
410
512
  experimental_customMergeAllOf,
411
513
  )
412
514
  ) {
413
515
  schemaUtils = createSchemaUtils<T, S, F>(
414
- props.validator,
415
- rootSchema,
516
+ validator,
517
+ schema,
416
518
  experimental_defaultFormStateBehavior,
417
519
  experimental_customMergeAllOf,
418
520
  );
419
521
  }
420
- const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;
522
+
523
+ const rootSchema = schemaUtils.getRootSchema();
524
+
525
+ // Compute the formData for getDefaultFormState() function based on the inputFormData, isUncontrolled and state
526
+ let defaultsFormData = inputFormData;
527
+ if (inputFormData === IS_RESET) {
528
+ defaultsFormData = undefined;
529
+ } else if (inputFormData === undefined && isUncontrolled) {
530
+ defaultsFormData = state.formData;
531
+ }
532
+ const formData: T = schemaUtils.getDefaultFormState(
533
+ rootSchema,
534
+ defaultsFormData,
535
+ false,
536
+ state.initialDefaultsGenerated,
537
+ ) as T;
421
538
  const _retrievedSchema = this.updateRetrievedSchema(
422
- retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData),
539
+ retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData),
423
540
  );
424
541
 
425
542
  const getCurrentErrors = (): ValidationData<T> => {
@@ -442,27 +559,30 @@ export default class Form<
442
559
  let errorSchema: ErrorSchema<T> | undefined;
443
560
  let schemaValidationErrors: RJSFValidationError[] = state.schemaValidationErrors;
444
561
  let schemaValidationErrorSchema: ErrorSchema<T> = state.schemaValidationErrorSchema;
445
- if (mustValidate) {
446
- const schemaValidation = this.validate(formData, schema, schemaUtils, _retrievedSchema);
447
- errors = schemaValidation.errors;
448
- // If retrievedSchema is undefined which means the schema or formData has changed, we do not merge state.
449
- // Else in the case where it hasn't changed, we merge 'state.errorSchema' with 'schemaValidation.errorSchema.' This done to display the raised field error.
450
- if (retrievedSchema === undefined) {
451
- errorSchema = schemaValidation.errorSchema;
452
- } else {
453
- errorSchema = mergeObjects(
454
- this.state?.errorSchema,
455
- schemaValidation.errorSchema,
456
- 'preventDuplicates',
457
- ) as ErrorSchema<T>;
458
- }
459
- schemaValidationErrors = errors;
460
- schemaValidationErrorSchema = errorSchema;
562
+ // If we are skipping live validate, it means that the state has already been updated with live validation errors
563
+ if (mustValidate && !skipLiveValidate) {
564
+ const liveValidation = this.liveValidate(
565
+ rootSchema,
566
+ schemaUtils,
567
+ state.errorSchema,
568
+ formData,
569
+ undefined,
570
+ state.customErrors,
571
+ retrievedSchema,
572
+ // If retrievedSchema is undefined which means the schema or formData has changed, we do not merge state.
573
+ // Else in the case where it hasn't changed,
574
+ retrievedSchema !== undefined,
575
+ );
576
+ errors = liveValidation.errors;
577
+ errorSchema = liveValidation.errorSchema;
578
+ schemaValidationErrors = liveValidation.schemaValidationErrors;
579
+ schemaValidationErrorSchema = liveValidation.schemaValidationErrorSchema;
461
580
  } else {
462
581
  const currentErrors = getCurrentErrors();
463
582
  errors = currentErrors.errors;
464
583
  errorSchema = currentErrors.errorSchema;
465
- if (formDataChangedFields.length > 0) {
584
+ // We only update the error schema for changed fields if mustValidate is false
585
+ if (formDataChangedFields.length > 0 && !mustValidate) {
466
586
  const newErrorSchema = formDataChangedFields.reduce(
467
587
  (acc, key) => {
468
588
  acc[key] = undefined;
@@ -476,25 +596,24 @@ export default class Form<
476
596
  'preventDuplicates',
477
597
  ) as ErrorSchema<T>;
478
598
  }
599
+ const mergedErrors = this.mergeErrors({ errorSchema, errors }, props.extraErrors, state.customErrors);
600
+ errors = mergedErrors.errors;
601
+ errorSchema = mergedErrors.errorSchema;
479
602
  }
480
603
 
481
- if (props.extraErrors) {
482
- const merged = validationDataMerge({ errorSchema, errors }, props.extraErrors);
483
- errorSchema = merged.errorSchema;
484
- errors = merged.errors;
485
- }
486
- const idSchema = schemaUtils.toIdSchema(
487
- _retrievedSchema,
488
- uiSchema['ui:rootFieldId'],
489
- formData,
490
- props.idPrefix,
491
- props.idSeparator,
492
- );
604
+ // Only store a new registry when the props cause a different one to be created
605
+ const newRegistry = this.getRegistry(props, rootSchema, schemaUtils);
606
+ const registry = deepEquals(state.registry, newRegistry) ? state.registry : newRegistry;
607
+ // Only compute a new `fieldPathId` when the `idPrefix` is different than the existing fieldPathId's ID_KEY
608
+ const fieldPathId =
609
+ state.fieldPathId && state.fieldPathId?.[ID_KEY] === registry.globalFormOptions.idPrefix
610
+ ? state.fieldPathId
611
+ : toFieldPathId('', registry.globalFormOptions);
493
612
  const nextState: FormState<T, S, F> = {
494
613
  schemaUtils,
495
- schema,
614
+ schema: rootSchema,
496
615
  uiSchema,
497
- idSchema,
616
+ fieldPathId,
498
617
  formData,
499
618
  edit,
500
619
  errors,
@@ -502,6 +621,8 @@ export default class Form<
502
621
  schemaValidationErrors,
503
622
  schemaValidationErrorSchema,
504
623
  retrievedSchema: _retrievedSchema,
624
+ initialDefaultsGenerated: true,
625
+ registry,
505
626
  };
506
627
  return nextState;
507
628
  }
@@ -513,23 +634,8 @@ export default class Form<
513
634
  * @returns - True if the component should be updated, false otherwise
514
635
  */
515
636
  shouldComponentUpdate(nextProps: FormProps<T, S, F>, nextState: FormState<T, S, F>): boolean {
516
- return shouldRender(this, nextProps, nextState);
517
- }
518
-
519
- /** Gets the previously raised customValidate errors.
520
- *
521
- * @returns the previous customValidate errors
522
- */
523
- private getPreviousCustomValidateErrors(): ErrorSchema<T> {
524
- const { customValidate, uiSchema } = this.props;
525
- const prevFormData = this.state.formData as T;
526
- let customValidateErrors = {};
527
- if (typeof customValidate === 'function') {
528
- const errorHandler = customValidate(prevFormData, createErrorHandler<T>(prevFormData), uiSchema);
529
- const userErrorSchema = unwrapErrorHandler<T>(errorHandler);
530
- customValidateErrors = userErrorSchema;
531
- }
532
- return customValidateErrors;
637
+ const { experimental_componentUpdateStrategy = 'customDeep' } = this.props;
638
+ return shouldRender(this, nextProps, nextState, experimental_componentUpdateStrategy);
533
639
  }
534
640
 
535
641
  /** Validates the `formData` against the `schema` using the `altSchemaUtils` (if provided otherwise it uses the
@@ -537,11 +643,12 @@ export default class Form<
537
643
  *
538
644
  * @param formData - The new form data to validate
539
645
  * @param schema - The schema used to validate against
540
- * @param altSchemaUtils - The alternate schemaUtils to use for validation
646
+ * @param [altSchemaUtils] - The alternate schemaUtils to use for validation
647
+ * @param [retrievedSchema] - An optionally retrieved schema for per
541
648
  */
542
649
  validate(
543
650
  formData: T | undefined,
544
- schema = this.props.schema,
651
+ schema = this.state.schema,
545
652
  altSchemaUtils?: SchemaUtilsType<T, S, F>,
546
653
  retrievedSchema?: S,
547
654
  ): ValidationData<T> {
@@ -556,7 +663,6 @@ export default class Form<
556
663
  /** Renders any errors contained in the `state` in using the `ErrorList`, if not disabled by `showErrorList`. */
557
664
  renderErrors(registry: Registry<T, S, F>) {
558
665
  const { errors, errorSchema, schema, uiSchema } = this.state;
559
- const { formContext } = this.props;
560
666
  const options = getUiOptions<T, S, F>(uiSchema);
561
667
  const ErrorListTemplate = getTemplate<'ErrorListTemplate', T, S, F>('ErrorListTemplate', registry, options);
562
668
 
@@ -567,7 +673,6 @@ export default class Form<
567
673
  errorSchema={errorSchema || {}}
568
674
  schema={schema}
569
675
  uiSchema={uiSchema}
570
- formContext={formContext}
571
676
  registry={registry}
572
677
  />
573
678
  );
@@ -575,6 +680,75 @@ export default class Form<
575
680
  return null;
576
681
  }
577
682
 
683
+ /** Merges any `extraErrors` or `customErrors` into the given `schemaValidation` object, returning the result
684
+ *
685
+ * @param schemaValidation - The `ValidationData` object into which additional errors are merged
686
+ * @param [extraErrors] - The extra errors from the props
687
+ * @param [customErrors] - The customErrors from custom components
688
+ * @return - The `extraErrors` and `customErrors` merged into the `schemaValidation`
689
+ * @private
690
+ */
691
+ private mergeErrors(
692
+ schemaValidation: ValidationData<T>,
693
+ extraErrors?: FormProps['extraErrors'],
694
+ customErrors?: ErrorSchemaBuilder,
695
+ ): ValidationData<T> {
696
+ let errorSchema: ErrorSchema<T> = schemaValidation.errorSchema;
697
+ let errors: RJSFValidationError[] = schemaValidation.errors;
698
+ if (extraErrors) {
699
+ const merged = validationDataMerge(schemaValidation, extraErrors);
700
+ errorSchema = merged.errorSchema;
701
+ errors = merged.errors;
702
+ }
703
+ if (customErrors) {
704
+ const merged = validationDataMerge(schemaValidation, customErrors.ErrorSchema, true);
705
+ errorSchema = merged.errorSchema;
706
+ errors = merged.errors;
707
+ }
708
+ return { errors, errorSchema };
709
+ }
710
+
711
+ /** Performs live validation and then updates and returns the errors and error schemas by potentially merging in
712
+ * `extraErrors` and `customErrors`.
713
+ *
714
+ * @param rootSchema - The `rootSchema` from the state
715
+ * @param schemaUtils - The `SchemaUtilsType` from the state
716
+ * @param originalErrorSchema - The original `ErrorSchema` from the state
717
+ * @param [formData] - The new form data to validate
718
+ * @param [extraErrors] - The extra errors from the props
719
+ * @param [customErrors] - The customErrors from custom components
720
+ * @param [retrievedSchema] - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`
721
+ * @param [mergeIntoOriginalErrorSchema=false] - Optional flag indicating whether we merge into original schema
722
+ * @returns - An object containing `errorSchema`, `errors`, `schemaValidationErrors` and `schemaValidationErrorSchema`
723
+ * @private
724
+ */
725
+ private liveValidate(
726
+ rootSchema: S,
727
+ schemaUtils: SchemaUtilsType<T, S, F>,
728
+ originalErrorSchema: ErrorSchema<S>,
729
+ formData?: T,
730
+ extraErrors?: FormProps['extraErrors'],
731
+ customErrors?: ErrorSchemaBuilder<T>,
732
+ retrievedSchema?: S,
733
+ mergeIntoOriginalErrorSchema = false,
734
+ ) {
735
+ const schemaValidation = this.validate(formData, rootSchema, schemaUtils, retrievedSchema);
736
+ const errors = schemaValidation.errors;
737
+ let errorSchema = schemaValidation.errorSchema;
738
+ // We merge 'originalErrorSchema' with 'schemaValidation.errorSchema.'; This done to display the raised field error.
739
+ if (mergeIntoOriginalErrorSchema) {
740
+ errorSchema = mergeObjects(
741
+ originalErrorSchema,
742
+ schemaValidation.errorSchema,
743
+ 'preventDuplicates',
744
+ ) as ErrorSchema<T>;
745
+ }
746
+ const schemaValidationErrors = errors;
747
+ const schemaValidationErrorSchema = errorSchema;
748
+ const mergedErrors = this.mergeErrors({ errorSchema, errors }, extraErrors, customErrors);
749
+ return { ...mergedErrors, schemaValidationErrors, schemaValidationErrorSchema };
750
+ }
751
+
578
752
  /** Returns the `formData` with only the elements specified in the `fields` list
579
753
  *
580
754
  * @param formData - The data for the `Form`
@@ -601,25 +775,28 @@ export default class Form<
601
775
  * @param [formData] - The form data to use while checking for empty objects/arrays
602
776
  */
603
777
  getFieldNames = (pathSchema: PathSchema<T>, formData?: T): string[][] => {
778
+ const formValueHasData = (value: T, isLeaf: boolean) =>
779
+ typeof value !== 'object' || _isEmpty(value) || (isLeaf && !_isEmpty(value));
604
780
  const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => {
605
- Object.keys(_obj).forEach((key: string) => {
606
- if (typeof _obj[key] === 'object') {
781
+ const objKeys = Object.keys(_obj);
782
+ objKeys.forEach((key: string) => {
783
+ const data = _obj[key];
784
+ if (typeof data === 'object') {
607
785
  const newPaths = paths.map((path) => [...path, key]);
608
786
  // If an object is marked with additionalProperties, all its keys are valid
609
- if (_obj[key][RJSF_ADDITIONAL_PROPERTIES_FLAG] && _obj[key][NAME_KEY] !== '') {
610
- acc.push(_obj[key][NAME_KEY]);
787
+ if (data[RJSF_ADDITIONAL_PROPERTIES_FLAG] && data[NAME_KEY] !== '') {
788
+ acc.push(data[NAME_KEY]);
611
789
  } else {
612
- getAllPaths(_obj[key], acc, newPaths);
790
+ getAllPaths(data, acc, newPaths);
613
791
  }
614
- } else if (key === NAME_KEY && _obj[key] !== '') {
792
+ } else if (key === NAME_KEY && data !== '') {
615
793
  paths.forEach((path) => {
616
794
  const formValue = _get(formData, path);
617
- // adds path to fieldNames if it points to a value
618
- // or an empty object/array
795
+ const isLeaf = objKeys.length === 1;
796
+ // adds path to fieldNames if it points to a value or an empty object/array which is not a leaf
619
797
  if (
620
- typeof formValue !== 'object' ||
621
- _isEmpty(formValue) ||
622
- (Array.isArray(formValue) && formValue.every((val) => typeof val !== 'object'))
798
+ formValueHasData(formValue, isLeaf) ||
799
+ (Array.isArray(formValue) && formValue.every((val) => formValueHasData(val, isLeaf)))
623
800
  ) {
624
801
  acc.push(path);
625
802
  }
@@ -642,124 +819,143 @@ export default class Form<
642
819
  const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
643
820
  const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData);
644
821
  const fieldNames = this.getFieldNames(pathSchema, formData);
645
- const newFormData = this.getUsedFormData(formData, fieldNames);
646
- return newFormData;
822
+ return this.getUsedFormData(formData, fieldNames);
647
823
  };
648
824
 
649
- // Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
650
- private filterErrorsBasedOnSchema(schemaErrors: ErrorSchema<T>, resolvedSchema?: S, formData?: any): ErrorSchema<T> {
651
- const { retrievedSchema, schemaUtils } = this.state;
652
- const _retrievedSchema = resolvedSchema ?? retrievedSchema;
653
- const pathSchema = schemaUtils.toPathSchema(_retrievedSchema, '', formData);
654
- const fieldNames = this.getFieldNames(pathSchema, formData);
655
- const filteredErrors: ErrorSchema<T> = _pick(schemaErrors, fieldNames as unknown as string[]);
656
- // If the root schema is of a primitive type, do not filter out the __errors
657
- if (resolvedSchema?.type !== 'object' && resolvedSchema?.type !== 'array') {
658
- filteredErrors.__errors = schemaErrors.__errors;
659
- }
660
-
661
- const prevCustomValidateErrors = this.getPreviousCustomValidateErrors();
662
- // Filtering out the previous raised customValidate errors so that they are cleared when no longer valid.
663
- const filterPreviousCustomErrors = (errors: string[] = [], prevCustomErrors: string[]) => {
664
- if (errors.length === 0) {
665
- return errors;
666
- }
667
-
668
- return errors.filter((error) => {
669
- return !prevCustomErrors.includes(error);
670
- });
671
- };
672
-
673
- // Removing undefined, null and empty errors.
674
- const filterNilOrEmptyErrors = (errors: any, previousCustomValidateErrors: any = {}): ErrorSchema<T> => {
675
- _forEach(errors, (errorAtKey: ErrorSchema<T>['__errors'] | undefined, errorKey: keyof typeof errors) => {
676
- const prevCustomValidateErrorAtKey: ErrorSchema<T> | undefined = previousCustomValidateErrors[errorKey];
677
- if (_isNil(errorAtKey) || (Array.isArray(errorAtKey) && errorAtKey.length === 0)) {
678
- delete errors[errorKey];
679
- } else if (
680
- isObject(errorAtKey) &&
681
- isObject(prevCustomValidateErrorAtKey) &&
682
- Array.isArray(prevCustomValidateErrorAtKey?.__errors)
683
- ) {
684
- // if previous customValidate error is an object and has __errors array, filter out the errors previous customValidate errors.
685
- errors[errorKey] = filterPreviousCustomErrors(errorAtKey.__errors, prevCustomValidateErrorAtKey.__errors);
686
- } else if (typeof errorAtKey === 'object' && !Array.isArray(errorAtKey.__errors)) {
687
- filterNilOrEmptyErrors(errorAtKey, previousCustomValidateErrors[errorKey]);
688
- }
689
- });
690
- return errors;
691
- };
692
- return filterNilOrEmptyErrors(filteredErrors, prevCustomValidateErrors);
693
- }
825
+ /** Allows a user to set a value for the provided `fieldPath`, which must be either a dotted path to the field OR a
826
+ * `FieldPathList`. To set the root element, used either `''` or `[]` for the path. Passing undefined will clear the
827
+ * value in the field.
828
+ *
829
+ * @param fieldPath - Either a dotted path to the field or the `FieldPathList` to the field
830
+ * @param [newValue] - The new value for the field
831
+ */
832
+ setFieldValue = (fieldPath: string | FieldPathList, newValue?: T) => {
833
+ const { registry } = this.state;
834
+ const path = Array.isArray(fieldPath) ? fieldPath : fieldPath.split('.');
835
+ const fieldPathId = toFieldPathId('', registry.globalFormOptions, path);
836
+ this.onChange(newValue, path, undefined, fieldPathId[ID_KEY]);
837
+ };
694
838
 
695
- /** Function to handle changes made to a field in the `Form`. This handler receives an entirely new copy of the
696
- * `formData` along with a new `ErrorSchema`. It will first update the `formData` with any missing default fields and
697
- * then, if `omitExtraData` and `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not
698
- * in a form field. Then, the resulting formData will be validated if required. The state will be updated with the new
699
- * updated (potentially filtered) `formData`, any errors that resulted from validation. Finally the `onChange`
700
- * callback will be called if specified with the updated state.
839
+ /** Pushes the given change information into the `pendingChanges` array and then calls `processPendingChanges()` if
840
+ * the array only contains a single pending change.
701
841
  *
702
- * @param formData - The new form data from a change to a field
703
- * @param newErrorSchema - The new `ErrorSchema` based on the field change
704
- * @param id - The id of the field that caused the change
842
+ * @param newValue - The new form data from a change to a field
843
+ * @param path - The path to the change into which to set the formData
844
+ * @param [newErrorSchema] - The new `ErrorSchema` based on the field change
845
+ * @param [id] - The id of the field that caused the change
846
+ */
847
+ onChange = (newValue: T | undefined, path: FieldPathList, newErrorSchema?: ErrorSchema<T>, id?: string) => {
848
+ this.pendingChanges.push({ newValue, path, newErrorSchema, id });
849
+ if (this.pendingChanges.length === 1) {
850
+ this.processPendingChange();
851
+ }
852
+ };
853
+
854
+ /** Function to handle changes made to a field in the `Form`. This handler gets the first change from the
855
+ * `pendingChanges` list, containing the `newValue` for the `formData` and the `path` at which the `newValue` is to be
856
+ * updated, along with a new, optional `ErrorSchema` for that same `path` and potentially the `id` of the field being
857
+ * changed. It will first update the `formData` with any missing default fields and then, if `omitExtraData` and
858
+ * `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not in a form field. Then, the
859
+ * resulting `formData` will be validated if required. The state will be updated with the new updated (potentially
860
+ * filtered) `formData`, any errors that resulted from validation. Finally the `onChange` callback will be called, if
861
+ * specified, with the updated state and the `processPendingChange()` function is called again.
705
862
  */
706
- onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema<T>, id?: string) => {
863
+ processPendingChange() {
864
+ if (this.pendingChanges.length === 0) {
865
+ return;
866
+ }
867
+ const { newValue, path, id } = this.pendingChanges[0];
868
+ const { newErrorSchema } = this.pendingChanges[0];
707
869
  const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
708
- const { schemaUtils, schema } = this.state;
870
+ const { formData: oldFormData, schemaUtils, schema, fieldPathId, schemaValidationErrorSchema, errors } = this.state;
871
+ let { customErrors, errorSchema: originalErrorSchema } = this.state;
872
+ const rootPathId = fieldPathId.path[0] || '';
709
873
 
874
+ const isRootPath = !path || path.length === 0 || (path.length === 1 && path[0] === rootPathId);
710
875
  let retrievedSchema = this.state.retrievedSchema;
876
+ let formData = isRootPath ? newValue : _cloneDeep(oldFormData);
711
877
  if (isObject(formData) || Array.isArray(formData)) {
712
- const newState = this.getStateFromProps(this.props, formData);
878
+ if (!isRootPath) {
879
+ // If the newValue is not on the root path, then set it into the form data
880
+ _set(formData, path, newValue);
881
+ }
882
+ // Pass true to skip live validation in `getStateFromProps()` since we will do it a bit later
883
+ const newState = this.getStateFromProps(this.props, formData, undefined, undefined, undefined, true);
713
884
  formData = newState.formData;
714
885
  retrievedSchema = newState.retrievedSchema;
715
886
  }
716
887
 
717
- const mustValidate = !noValidate && liveValidate;
888
+ const mustValidate = !noValidate && (liveValidate === true || liveValidate === 'onChange');
718
889
  let state: Partial<FormState<T, S, F>> = { formData, schema };
719
890
  let newFormData = formData;
720
891
 
721
- if (omitExtraData === true && liveOmit === true) {
892
+ if (omitExtraData === true && (liveOmit === true || liveOmit === 'onChange')) {
722
893
  newFormData = this.omitExtraData(formData);
723
894
  state = {
724
895
  formData: newFormData,
725
896
  };
726
897
  }
727
898
 
728
- if (mustValidate) {
729
- const schemaValidation = this.validate(newFormData, schema, schemaUtils, retrievedSchema);
730
- let errors = schemaValidation.errors;
731
- let errorSchema = schemaValidation.errorSchema;
732
- const schemaValidationErrors = errors;
733
- const schemaValidationErrorSchema = errorSchema;
734
- if (extraErrors) {
735
- const merged = validationDataMerge(schemaValidation, extraErrors);
736
- errorSchema = merged.errorSchema;
737
- errors = merged.errors;
738
- }
739
- // Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
740
- if (newErrorSchema) {
741
- const filteredErrors = this.filterErrorsBasedOnSchema(newErrorSchema, retrievedSchema, newFormData);
742
- errorSchema = mergeObjects(errorSchema, filteredErrors, 'preventDuplicates') as ErrorSchema<T>;
899
+ if (newErrorSchema) {
900
+ // First check to see if there is an existing validation error on this path...
901
+ // @ts-expect-error TS2590, because getting from the error schema is confusing TS
902
+ const oldValidationError = !isRootPath ? _get(schemaValidationErrorSchema, path) : schemaValidationErrorSchema;
903
+ // If there is an old validation error for this path, assume we are updating it directly
904
+ if (!_isEmpty(oldValidationError)) {
905
+ // Update the originalErrorSchema "in place" or replace it if it is the root
906
+ if (!isRootPath) {
907
+ _set(originalErrorSchema, path, newErrorSchema);
908
+ } else {
909
+ originalErrorSchema = newErrorSchema;
910
+ }
911
+ } else {
912
+ if (!customErrors) {
913
+ customErrors = new ErrorSchemaBuilder<T>();
914
+ }
915
+ if (isRootPath) {
916
+ const errors = _get(newErrorSchema, ERRORS_KEY);
917
+ if (errors) {
918
+ // only set errors when there are some
919
+ customErrors.setErrors(errors);
920
+ }
921
+ } else {
922
+ _set(customErrors.ErrorSchema, path, newErrorSchema);
923
+ }
743
924
  }
744
- state = {
745
- formData: newFormData,
746
- errors,
747
- errorSchema,
748
- schemaValidationErrors,
749
- schemaValidationErrorSchema,
750
- };
925
+ } else if (customErrors && _get(customErrors.ErrorSchema, [...path, ERRORS_KEY])) {
926
+ // If we have custom errors and the path has an error, then we need to clear it
927
+ customErrors.clearErrors(path);
928
+ }
929
+ // If there are pending changes in the queue, skip live validation since it will happen with the last change
930
+ if (mustValidate && this.pendingChanges.length === 1) {
931
+ const liveValidation = this.liveValidate(
932
+ schema,
933
+ schemaUtils,
934
+ originalErrorSchema,
935
+ newFormData,
936
+ extraErrors,
937
+ customErrors,
938
+ retrievedSchema,
939
+ );
940
+ state = { formData: newFormData, ...liveValidation, customErrors };
751
941
  } else if (!noValidate && newErrorSchema) {
752
- const errorSchema = extraErrors
753
- ? (mergeObjects(newErrorSchema, extraErrors, 'preventDuplicates') as ErrorSchema<T>)
754
- : newErrorSchema;
942
+ // Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
943
+ const mergedErrors = this.mergeErrors({ errorSchema: originalErrorSchema, errors }, extraErrors, customErrors);
755
944
  state = {
756
945
  formData: newFormData,
757
- errorSchema: errorSchema,
758
- errors: toErrorList(errorSchema),
946
+ ...mergedErrors,
947
+ customErrors,
759
948
  };
760
949
  }
761
- this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id));
762
- };
950
+ this.setState(state as FormState<T, S, F>, () => {
951
+ if (onChange) {
952
+ onChange(toIChangeEvent({ ...this.state, ...state }), id);
953
+ }
954
+ // Now remove the change we just completed and call this again
955
+ this.pendingChanges.shift();
956
+ this.processPendingChange();
957
+ });
958
+ }
763
959
 
764
960
  /**
765
961
  * If the retrievedSchema has changed the new retrievedSchema is returned.
@@ -782,8 +978,16 @@ export default class Form<
782
978
  *
783
979
  */
784
980
  reset = () => {
785
- const { onChange } = this.props;
786
- const newState = this.getStateFromProps(this.props, undefined);
981
+ // Cast the IS_RESET symbol to T to avoid type issues, we use this symbol to detect reset mode
982
+ const { formData: propsFormData, initialFormData = IS_RESET as T, onChange } = this.props;
983
+ const newState = this.getStateFromProps(
984
+ this.props,
985
+ propsFormData ?? initialFormData,
986
+ undefined,
987
+ undefined,
988
+ undefined,
989
+ true,
990
+ );
787
991
  const newFormData = newState.formData;
788
992
  const state = {
789
993
  formData: newFormData,
@@ -791,22 +995,61 @@ export default class Form<
791
995
  errors: [] as unknown,
792
996
  schemaValidationErrors: [] as unknown,
793
997
  schemaValidationErrorSchema: {},
998
+ initialDefaultsGenerated: false,
999
+ customErrors: undefined,
794
1000
  } as FormState<T, S, F>;
795
1001
 
796
- this.setState(state, () => onChange && onChange({ ...this.state, ...state }));
1002
+ this.setState(state, () => onChange && onChange(toIChangeEvent({ ...this.state, ...state })));
797
1003
  };
798
1004
 
799
1005
  /** Callback function to handle when a field on the form is blurred. Calls the `onBlur` callback for the `Form` if it
800
- * was provided.
1006
+ * was provided. Also runs any live validation and/or live omit operations if the flags indicate they should happen
1007
+ * during `onBlur`.
801
1008
  *
802
1009
  * @param id - The unique `id` of the field that was blurred
803
1010
  * @param data - The data associated with the field that was blurred
804
1011
  */
805
1012
  onBlur = (id: string, data: any) => {
806
- const { onBlur } = this.props;
1013
+ const { onBlur, omitExtraData, liveOmit, liveValidate } = this.props;
807
1014
  if (onBlur) {
808
1015
  onBlur(id, data);
809
1016
  }
1017
+ if ((omitExtraData === true && liveOmit === 'onBlur') || liveValidate === 'onBlur') {
1018
+ const { onChange, extraErrors } = this.props;
1019
+ const { formData } = this.state;
1020
+ let newFormData: T | undefined = formData;
1021
+ let state: Partial<FormState<T, S, F>> = { formData: newFormData };
1022
+ if (omitExtraData === true && liveOmit === 'onBlur') {
1023
+ newFormData = this.omitExtraData(formData);
1024
+ state = { formData: newFormData };
1025
+ }
1026
+ if (liveValidate === 'onBlur') {
1027
+ const { schema, schemaUtils, errorSchema, customErrors, retrievedSchema } = this.state;
1028
+ const liveValidation = this.liveValidate(
1029
+ schema,
1030
+ schemaUtils,
1031
+ errorSchema,
1032
+ newFormData,
1033
+ extraErrors,
1034
+ customErrors,
1035
+ retrievedSchema,
1036
+ );
1037
+ state = { formData: newFormData, ...liveValidation, customErrors };
1038
+ }
1039
+ const hasChanges = Object.keys(state)
1040
+ // Filter out `schemaValidationErrors` and `schemaValidationErrorSchema` since they aren't IChangeEvent props
1041
+ .filter((key) => !key.startsWith('schemaValidation'))
1042
+ .some((key) => {
1043
+ const oldData = _get(this.state, key);
1044
+ const newData = _get(state, key);
1045
+ return !deepEquals(oldData, newData);
1046
+ });
1047
+ this.setState(state as FormState<T, S, F>, () => {
1048
+ if (onChange && hasChanges) {
1049
+ onChange(toIChangeEvent({ ...this.state, ...state }), id);
1050
+ }
1051
+ });
1052
+ }
810
1053
  };
811
1054
 
812
1055
  /** Callback function to handle when a field on the form is focused. Calls the `onFocus` callback for the `Form` if it
@@ -859,34 +1102,60 @@ export default class Form<
859
1102
  },
860
1103
  () => {
861
1104
  if (onSubmit) {
862
- onSubmit({ ...this.state, formData: newFormData, status: 'submitted' }, event);
1105
+ onSubmit(toIChangeEvent({ ...this.state, formData: newFormData }, 'submitted'), event);
863
1106
  }
864
1107
  },
865
1108
  );
866
1109
  }
867
1110
  };
868
1111
 
869
- /** Returns the registry for the form */
870
- getRegistry(): Registry<T, S, F> {
871
- const { translateString: customTranslateString, uiSchema = {} } = this.props;
872
- const { schemaUtils } = this.state;
1112
+ /** Extracts the `GlobalFormOptions` from the given Form `props`
1113
+ *
1114
+ * @param props - The form props to extract the global form options from
1115
+ * @returns - The `GlobalFormOptions` from the props
1116
+ * @private
1117
+ */
1118
+ private getGlobalFormOptions(props: FormProps<T, S, F>): GlobalFormOptions {
1119
+ const {
1120
+ uiSchema = {},
1121
+ experimental_componentUpdateStrategy,
1122
+ idSeparator = DEFAULT_ID_SEPARATOR,
1123
+ idPrefix = DEFAULT_ID_PREFIX,
1124
+ nameGenerator,
1125
+ useFallbackUiForUnsupportedType = false,
1126
+ } = props;
1127
+ const rootFieldId = uiSchema['ui:rootFieldId'];
1128
+ // Omit any options that are undefined or null
1129
+ return {
1130
+ idPrefix: rootFieldId || idPrefix,
1131
+ idSeparator,
1132
+ useFallbackUiForUnsupportedType,
1133
+ ...(experimental_componentUpdateStrategy !== undefined && { experimental_componentUpdateStrategy }),
1134
+ ...(nameGenerator !== undefined && { nameGenerator }),
1135
+ };
1136
+ }
1137
+
1138
+ /** Computed the registry for the form using the given `props`, `schema` and `schemaUtils` */
1139
+ getRegistry(props: FormProps<T, S, F>, schema: S, schemaUtils: SchemaUtilsType<T, S, F>): Registry<T, S, F> {
1140
+ const { translateString: customTranslateString, uiSchema = {} } = props;
873
1141
  const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry<T, S, F>();
874
1142
  return {
875
- fields: { ...fields, ...this.props.fields },
1143
+ fields: { ...fields, ...props.fields },
876
1144
  templates: {
877
1145
  ...templates,
878
- ...this.props.templates,
1146
+ ...props.templates,
879
1147
  ButtonTemplates: {
880
1148
  ...templates.ButtonTemplates,
881
- ...this.props.templates?.ButtonTemplates,
1149
+ ...props.templates?.ButtonTemplates,
882
1150
  },
883
1151
  },
884
- widgets: { ...widgets, ...this.props.widgets },
885
- rootSchema: this.props.schema,
886
- formContext: this.props.formContext || formContext,
1152
+ widgets: { ...widgets, ...props.widgets },
1153
+ rootSchema: schema,
1154
+ formContext: props.formContext || formContext,
887
1155
  schemaUtils,
888
1156
  translateString: customTranslateString || translateString,
889
1157
  globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
1158
+ globalFormOptions: this.getGlobalFormOptions(props),
890
1159
  };
891
1160
  }
892
1161
 
@@ -1011,8 +1280,6 @@ export default class Form<
1011
1280
  const {
1012
1281
  children,
1013
1282
  id,
1014
- idPrefix,
1015
- idSeparator,
1016
1283
  className = '',
1017
1284
  tagName,
1018
1285
  name,
@@ -1025,13 +1292,11 @@ export default class Form<
1025
1292
  noHtml5Validate = false,
1026
1293
  disabled,
1027
1294
  readonly,
1028
- formContext,
1029
1295
  showErrorList = 'top',
1030
1296
  _internalFormWrapper,
1031
1297
  } = this.props;
1032
1298
 
1033
- const { schema, uiSchema, formData, errorSchema, idSchema } = this.state;
1034
- const registry = this.getRegistry();
1299
+ const { schema, uiSchema, formData, errorSchema, fieldPathId, registry } = this.state;
1035
1300
  const { SchemaField: _SchemaField } = registry.fields;
1036
1301
  const { SubmitButton } = registry.templates.ButtonTemplates;
1037
1302
  // The `semantic-ui` and `material-ui` themes have `_internalFormWrapper`s that take an `as` prop that is the
@@ -1068,10 +1333,7 @@ export default class Form<
1068
1333
  schema={schema}
1069
1334
  uiSchema={uiSchema}
1070
1335
  errorSchema={errorSchema}
1071
- idSchema={idSchema}
1072
- idPrefix={idPrefix}
1073
- idSeparator={idSeparator}
1074
- formContext={formContext}
1336
+ fieldPathId={fieldPathId}
1075
1337
  formData={formData}
1076
1338
  onChange={this.onChange}
1077
1339
  onBlur={this.onBlur}