@rjsf/core 6.0.0-beta.2 → 6.0.0-beta.21

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 (114) hide show
  1. package/dist/core.umd.js +705 -471
  2. package/dist/{index.js → index.cjs} +1094 -844
  3. package/dist/index.cjs.map +7 -0
  4. package/dist/index.esm.js +1053 -774
  5. package/dist/index.esm.js.map +4 -4
  6. package/lib/components/Form.d.ts +88 -23
  7. package/lib/components/Form.d.ts.map +1 -1
  8. package/lib/components/Form.js +213 -151
  9. package/lib/components/fields/ArrayField.d.ts +17 -7
  10. package/lib/components/fields/ArrayField.d.ts.map +1 -1
  11. package/lib/components/fields/ArrayField.js +116 -70
  12. package/lib/components/fields/BooleanField.d.ts.map +1 -1
  13. package/lib/components/fields/BooleanField.js +7 -2
  14. package/lib/components/fields/LayoutGridField.d.ts +27 -25
  15. package/lib/components/fields/LayoutGridField.d.ts.map +1 -1
  16. package/lib/components/fields/LayoutGridField.js +83 -59
  17. package/lib/components/fields/LayoutHeaderField.d.ts +1 -1
  18. package/lib/components/fields/LayoutHeaderField.js +3 -3
  19. package/lib/components/fields/LayoutMultiSchemaField.js +6 -5
  20. package/lib/components/fields/MultiSchemaField.d.ts.map +1 -1
  21. package/lib/components/fields/MultiSchemaField.js +13 -9
  22. package/lib/components/fields/NullField.js +3 -3
  23. package/lib/components/fields/NumberField.d.ts.map +1 -1
  24. package/lib/components/fields/NumberField.js +3 -3
  25. package/lib/components/fields/ObjectField.d.ts +3 -3
  26. package/lib/components/fields/ObjectField.d.ts.map +1 -1
  27. package/lib/components/fields/ObjectField.js +34 -34
  28. package/lib/components/fields/OptionalDataControlsField.d.ts +8 -0
  29. package/lib/components/fields/OptionalDataControlsField.d.ts.map +1 -0
  30. package/lib/components/fields/OptionalDataControlsField.js +43 -0
  31. package/lib/components/fields/SchemaField.d.ts.map +1 -1
  32. package/lib/components/fields/SchemaField.js +17 -17
  33. package/lib/components/fields/StringField.d.ts.map +1 -1
  34. package/lib/components/fields/StringField.js +7 -2
  35. package/lib/components/fields/index.d.ts.map +1 -1
  36. package/lib/components/fields/index.js +2 -0
  37. package/lib/components/templates/ArrayFieldDescriptionTemplate.d.ts +1 -1
  38. package/lib/components/templates/ArrayFieldDescriptionTemplate.js +3 -3
  39. package/lib/components/templates/ArrayFieldItemButtonsTemplate.js +2 -2
  40. package/lib/components/templates/ArrayFieldTemplate.d.ts.map +1 -1
  41. package/lib/components/templates/ArrayFieldTemplate.js +4 -3
  42. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts +1 -1
  43. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts.map +1 -1
  44. package/lib/components/templates/ArrayFieldTitleTemplate.js +3 -3
  45. package/lib/components/templates/ButtonTemplates/AddButton.d.ts +1 -1
  46. package/lib/components/templates/ButtonTemplates/AddButton.d.ts.map +1 -1
  47. package/lib/components/templates/ButtonTemplates/AddButton.js +2 -2
  48. package/lib/components/templates/FieldErrorTemplate.js +2 -2
  49. package/lib/components/templates/FieldHelpTemplate.js +2 -2
  50. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts +8 -0
  51. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts.map +1 -0
  52. package/lib/components/templates/MultiSchemaFieldTemplate.js +10 -0
  53. package/lib/components/templates/ObjectFieldTemplate.d.ts.map +1 -1
  54. package/lib/components/templates/ObjectFieldTemplate.js +3 -2
  55. package/lib/components/templates/OptionalDataControlsTemplate.d.ts +11 -0
  56. package/lib/components/templates/OptionalDataControlsTemplate.d.ts.map +1 -0
  57. package/lib/components/templates/OptionalDataControlsTemplate.js +20 -0
  58. package/lib/components/templates/TitleField.d.ts.map +1 -1
  59. package/lib/components/templates/TitleField.js +2 -2
  60. package/lib/components/templates/UnsupportedField.js +3 -3
  61. package/lib/components/templates/index.d.ts.map +1 -1
  62. package/lib/components/templates/index.js +4 -0
  63. package/lib/components/widgets/AltDateWidget.d.ts.map +1 -1
  64. package/lib/components/widgets/AltDateWidget.js +15 -18
  65. package/lib/components/widgets/CheckboxesWidget.js +2 -2
  66. package/lib/getDefaultRegistry.d.ts.map +1 -1
  67. package/lib/getDefaultRegistry.js +2 -1
  68. package/lib/getTestRegistry.d.ts +5 -0
  69. package/lib/getTestRegistry.d.ts.map +1 -0
  70. package/lib/getTestRegistry.js +19 -0
  71. package/lib/index.d.ts +2 -1
  72. package/lib/index.d.ts.map +1 -1
  73. package/lib/index.js +2 -1
  74. package/lib/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +18 -19
  76. package/src/components/Form.tsx +306 -177
  77. package/src/components/fields/ArrayField.tsx +127 -80
  78. package/src/components/fields/BooleanField.tsx +12 -3
  79. package/src/components/fields/LayoutGridField.tsx +95 -88
  80. package/src/components/fields/LayoutHeaderField.tsx +3 -3
  81. package/src/components/fields/LayoutMultiSchemaField.tsx +5 -5
  82. package/src/components/fields/MultiSchemaField.tsx +51 -35
  83. package/src/components/fields/NullField.tsx +3 -3
  84. package/src/components/fields/NumberField.tsx +11 -3
  85. package/src/components/fields/ObjectField.tsx +47 -53
  86. package/src/components/fields/OptionalDataControlsField.tsx +84 -0
  87. package/src/components/fields/SchemaField.tsx +24 -30
  88. package/src/components/fields/StringField.tsx +12 -3
  89. package/src/components/fields/index.ts +2 -0
  90. package/src/components/templates/ArrayFieldDescriptionTemplate.tsx +3 -3
  91. package/src/components/templates/ArrayFieldItemButtonsTemplate.tsx +5 -5
  92. package/src/components/templates/ArrayFieldTemplate.tsx +9 -5
  93. package/src/components/templates/ArrayFieldTitleTemplate.tsx +4 -3
  94. package/src/components/templates/BaseInputTemplate.tsx +3 -3
  95. package/src/components/templates/ButtonTemplates/AddButton.tsx +2 -0
  96. package/src/components/templates/FieldErrorTemplate.tsx +2 -2
  97. package/src/components/templates/FieldHelpTemplate.tsx +2 -2
  98. package/src/components/templates/MultiSchemaFieldTemplate.tsx +20 -0
  99. package/src/components/templates/ObjectFieldTemplate.tsx +10 -5
  100. package/src/components/templates/OptionalDataControlsTemplate.tsx +43 -0
  101. package/src/components/templates/TitleField.tsx +6 -1
  102. package/src/components/templates/UnsupportedField.tsx +3 -3
  103. package/src/components/templates/WrapIfAdditionalTemplate.tsx +1 -1
  104. package/src/components/templates/index.ts +4 -0
  105. package/src/components/widgets/AltDateWidget.tsx +21 -23
  106. package/src/components/widgets/CheckboxWidget.tsx +2 -2
  107. package/src/components/widgets/CheckboxesWidget.tsx +3 -3
  108. package/src/components/widgets/RadioWidget.tsx +1 -1
  109. package/src/components/widgets/SelectWidget.tsx +1 -1
  110. package/src/components/widgets/TextareaWidget.tsx +1 -1
  111. package/src/getDefaultRegistry.ts +10 -1
  112. package/src/getTestRegistry.tsx +34 -0
  113. package/src/index.ts +2 -1
  114. 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,14 +38,16 @@ 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,
40
45
  } from '@rjsf/utils';
41
- import _forEach from 'lodash/forEach';
46
+ import _cloneDeep from 'lodash/cloneDeep';
42
47
  import _get from 'lodash/get';
43
48
  import _isEmpty from 'lodash/isEmpty';
44
- import _isNil from 'lodash/isNil';
45
49
  import _pick from 'lodash/pick';
50
+ import _set from 'lodash/set';
46
51
  import _toPath from 'lodash/toPath';
47
52
 
48
53
  import getDefaultRegistry from '../getDefaultRegistry';
@@ -195,6 +200,17 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
195
200
  * `emptyObjectFields`
196
201
  */
197
202
  experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
203
+ /**
204
+ * Controls the component update strategy used by the Form's `shouldComponentUpdate` lifecycle method.
205
+ *
206
+ * - `'customDeep'`: Uses RJSF's custom deep equality checks via the `deepEquals` utility function,
207
+ * which treats all functions as equivalent and provides optimized performance for form data comparisons.
208
+ * - `'shallow'`: Uses shallow comparison of props and state (only compares direct properties). This matches React's PureComponent behavior.
209
+ * - `'always'`: Always rerenders when called. This matches React's Component behavior.
210
+ *
211
+ * @default 'customDeep'
212
+ */
213
+ experimental_componentUpdateStrategy?: 'customDeep' | 'shallow' | 'always';
198
214
  /** Optional function that allows for custom merging of `allOf` schemas
199
215
  */
200
216
  experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
@@ -226,10 +242,10 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
226
242
  schema: S;
227
243
  /** The uiSchema for the form */
228
244
  uiSchema: UiSchema<T, S, F>;
229
- /** The `IdSchema` for the form, computed from the `schema`, the `rootFieldId`, the `formData` and the `idPrefix` and
245
+ /** The `FieldPathId` for the form, computed from the `schema`, the `rootFieldId`, the `idPrefix` and
230
246
  * `idSeparator` props.
231
247
  */
232
- idSchema: IdSchema<T>;
248
+ fieldPathId: FieldPathId;
233
249
  /** The schemaUtils implementation used by the `Form`, created from the `validator` and the `schema` */
234
250
  schemaUtils: SchemaUtilsType<T, S, F>;
235
251
  /** The current data for the form, computed from the `formData` prop and the changes made by the user */
@@ -240,26 +256,50 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
240
256
  errors: RJSFValidationError[];
241
257
  /** The current errors, in `ErrorSchema` format, for the form, includes `extraErrors` */
242
258
  errorSchema: ErrorSchema<T>;
259
+ // Private
243
260
  /** The current list of errors for the form directly from schema validation, does NOT include `extraErrors` */
244
261
  schemaValidationErrors: RJSFValidationError[];
245
262
  /** The current errors, in `ErrorSchema` format, for the form directly from schema validation, does NOT include
246
263
  * `extraErrors`
247
264
  */
248
265
  schemaValidationErrorSchema: ErrorSchema<T>;
249
- // Private
266
+ /** A container used to handle custom errors provided via `onChange` */
267
+ customErrors?: ErrorSchemaBuilder<T>;
250
268
  /** @description result of schemaUtils.retrieveSchema(schema, formData). This a memoized value to avoid re calculate at internal functions (getStateFromProps, onChange) */
251
269
  retrievedSchema: S;
270
+ /** Flag indicating whether the initial form defaults have been generated */
271
+ initialDefaultsGenerated: boolean;
252
272
  }
253
273
 
254
274
  /** The event data passed when changes have been made to the form, includes everything from the `FormState` except
255
275
  * the schema validation errors. An additional `status` is added when returned from `onSubmit`
256
276
  */
257
277
  export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
258
- extends Omit<FormState<T, S, F>, 'schemaValidationErrors' | 'schemaValidationErrorSchema'> {
278
+ extends Omit<
279
+ FormState<T, S, F>,
280
+ | 'schemaValidationErrors'
281
+ | 'schemaValidationErrorSchema'
282
+ | 'retrievedSchema'
283
+ | 'customErrors'
284
+ | 'initialDefaultsGenerated'
285
+ > {
259
286
  /** The status of the form when submitted */
260
287
  status?: 'submitted';
261
288
  }
262
289
 
290
+ /** The definition of a pending change that will be processed in the `onChange` handler
291
+ */
292
+ interface PendingChange<T> {
293
+ /** The path into the formData/errorSchema at which the `newValue`/`newErrorSchema` will be set */
294
+ path: FieldPathList;
295
+ /** The new value to set into the formData */
296
+ newValue?: T;
297
+ /** The new errors to be set into the errorSchema, if any */
298
+ newErrorSchema?: ErrorSchema<T>;
299
+ /** The optional id of the field for which the change is being made */
300
+ id?: string;
301
+ }
302
+
263
303
  /** The `Form` component renders the outer form and all the fields defined in the `schema` */
264
304
  export default class Form<
265
305
  T = any,
@@ -271,6 +311,10 @@ export default class Form<
271
311
  */
272
312
  formElement: RefObject<any>;
273
313
 
314
+ /** The list of pending changes
315
+ */
316
+ pendingChanges: PendingChange<T>[] = [];
317
+
274
318
  /** Constructs the `Form` from the `props`. Will setup the initial state from the props. It will also call the
275
319
  * `onChange` handler if the initially provided `formData` is modified to add missing default values as part of the
276
320
  * state construction.
@@ -314,12 +358,18 @@ export default class Form<
314
358
  prevState: FormState<T, S, F>,
315
359
  ): { nextState: FormState<T, S, F>; shouldUpdate: true } | { shouldUpdate: false } {
316
360
  if (!deepEquals(this.props, prevProps)) {
361
+ // Compare the previous props formData against the current props formData
317
362
  const formDataChangedFields = getChangedFields(this.props.formData, prevProps.formData);
363
+ // Compare the current props formData against the current state's formData to determine if the new props were the
364
+ // result of the onChange from the existing state formData
365
+ const stateDataChangedFields = getChangedFields(this.props.formData, this.state.formData);
318
366
  const isSchemaChanged = !deepEquals(prevProps.schema, this.props.schema);
319
367
  // When formData is not an object, getChangedFields returns an empty array.
320
368
  // In this case, deepEquals is most needed to check again.
321
369
  const isFormDataChanged =
322
370
  formDataChangedFields.length > 0 || !deepEquals(prevProps.formData, this.props.formData);
371
+ const isStateDataChanged =
372
+ stateDataChangedFields.length > 0 || !deepEquals(this.state.formData, this.props.formData);
323
373
  const nextState = this.getStateFromProps(
324
374
  this.props,
325
375
  this.props.formData,
@@ -329,6 +379,8 @@ export default class Form<
329
379
  isSchemaChanged || isFormDataChanged ? undefined : this.state.retrievedSchema,
330
380
  isSchemaChanged,
331
381
  formDataChangedFields,
382
+ // Skip live validation for this request if no form data has changed from the last state
383
+ !isStateDataChanged,
332
384
  );
333
385
  const shouldUpdate = !deepEquals(nextState, prevState);
334
386
  return { nextState, shouldUpdate };
@@ -376,6 +428,7 @@ export default class Form<
376
428
  * @param retrievedSchema - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`.
377
429
  * @param isSchemaChanged - A flag indicating whether the schema has changed.
378
430
  * @param formDataChangedFields - The changed fields of `formData`
431
+ * @param skipLiveValidate - Optional flag, if true, means that we are not running live validation
379
432
  * @returns - The new state for the `Form`
380
433
  */
381
434
  getStateFromProps(
@@ -384,14 +437,15 @@ export default class Form<
384
437
  retrievedSchema?: S,
385
438
  isSchemaChanged = false,
386
439
  formDataChangedFields: string[] = [],
440
+ skipLiveValidate = false,
387
441
  ): FormState<T, S, F> {
388
442
  const state: FormState<T, S, F> = this.state || {};
389
443
  const schema = 'schema' in props ? props.schema : this.props.schema;
444
+ const validator = 'validator' in props ? props.validator : this.props.validator;
390
445
  const uiSchema: UiSchema<T, S, F> = ('uiSchema' in props ? props.uiSchema! : this.props.uiSchema!) || {};
391
446
  const edit = typeof inputFormData !== 'undefined';
392
447
  const liveValidate = 'liveValidate' in props ? props.liveValidate : this.props.liveValidate;
393
448
  const mustValidate = edit && !props.noValidate && liveValidate;
394
- const rootSchema = schema;
395
449
  const experimental_defaultFormStateBehavior =
396
450
  'experimental_defaultFormStateBehavior' in props
397
451
  ? props.experimental_defaultFormStateBehavior
@@ -404,22 +458,29 @@ export default class Form<
404
458
  if (
405
459
  !schemaUtils ||
406
460
  schemaUtils.doesSchemaUtilsDiffer(
407
- props.validator,
408
- rootSchema,
461
+ validator,
462
+ schema,
409
463
  experimental_defaultFormStateBehavior,
410
464
  experimental_customMergeAllOf,
411
465
  )
412
466
  ) {
413
467
  schemaUtils = createSchemaUtils<T, S, F>(
414
- props.validator,
415
- rootSchema,
468
+ validator,
469
+ schema,
416
470
  experimental_defaultFormStateBehavior,
417
471
  experimental_customMergeAllOf,
418
472
  );
419
473
  }
420
- const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;
474
+
475
+ const rootSchema = schemaUtils.getRootSchema();
476
+ const formData: T = schemaUtils.getDefaultFormState(
477
+ rootSchema,
478
+ inputFormData,
479
+ false,
480
+ state.initialDefaultsGenerated,
481
+ ) as T;
421
482
  const _retrievedSchema = this.updateRetrievedSchema(
422
- retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData),
483
+ retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData),
423
484
  );
424
485
 
425
486
  const getCurrentErrors = (): ValidationData<T> => {
@@ -442,27 +503,30 @@ export default class Form<
442
503
  let errorSchema: ErrorSchema<T> | undefined;
443
504
  let schemaValidationErrors: RJSFValidationError[] = state.schemaValidationErrors;
444
505
  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;
506
+ // If we are skipping live validate, it means that the state has already been updated with live validation errors
507
+ if (mustValidate && !skipLiveValidate) {
508
+ const liveValidation = this.liveValidate(
509
+ rootSchema,
510
+ schemaUtils,
511
+ state.errorSchema,
512
+ formData,
513
+ undefined,
514
+ state.customErrors,
515
+ retrievedSchema,
516
+ // If retrievedSchema is undefined which means the schema or formData has changed, we do not merge state.
517
+ // Else in the case where it hasn't changed,
518
+ retrievedSchema !== undefined,
519
+ );
520
+ errors = liveValidation.errors;
521
+ errorSchema = liveValidation.errorSchema;
522
+ schemaValidationErrors = liveValidation.schemaValidationErrors;
523
+ schemaValidationErrorSchema = liveValidation.schemaValidationErrorSchema;
461
524
  } else {
462
525
  const currentErrors = getCurrentErrors();
463
526
  errors = currentErrors.errors;
464
527
  errorSchema = currentErrors.errorSchema;
465
- if (formDataChangedFields.length > 0) {
528
+ // We only update the error schema for changed fields if mustValidate is false
529
+ if (formDataChangedFields.length > 0 && !mustValidate) {
466
530
  const newErrorSchema = formDataChangedFields.reduce(
467
531
  (acc, key) => {
468
532
  acc[key] = undefined;
@@ -476,25 +540,17 @@ export default class Form<
476
540
  'preventDuplicates',
477
541
  ) as ErrorSchema<T>;
478
542
  }
543
+ const mergedErrors = this.mergeErrors({ errorSchema, errors }, props.extraErrors, state.customErrors);
544
+ errors = mergedErrors.errors;
545
+ errorSchema = mergedErrors.errorSchema;
479
546
  }
480
547
 
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
- );
548
+ const fieldPathId = toFieldPathId('', this.getGlobalFormOptions(this.props));
493
549
  const nextState: FormState<T, S, F> = {
494
550
  schemaUtils,
495
- schema,
551
+ schema: rootSchema,
496
552
  uiSchema,
497
- idSchema,
553
+ fieldPathId,
498
554
  formData,
499
555
  edit,
500
556
  errors,
@@ -502,6 +558,7 @@ export default class Form<
502
558
  schemaValidationErrors,
503
559
  schemaValidationErrorSchema,
504
560
  retrievedSchema: _retrievedSchema,
561
+ initialDefaultsGenerated: true,
505
562
  };
506
563
  return nextState;
507
564
  }
@@ -513,23 +570,8 @@ export default class Form<
513
570
  * @returns - True if the component should be updated, false otherwise
514
571
  */
515
572
  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;
573
+ const { experimental_componentUpdateStrategy = 'customDeep' } = this.props;
574
+ return shouldRender(this, nextProps, nextState, experimental_componentUpdateStrategy);
533
575
  }
534
576
 
535
577
  /** Validates the `formData` against the `schema` using the `altSchemaUtils` (if provided otherwise it uses the
@@ -537,11 +579,12 @@ export default class Form<
537
579
  *
538
580
  * @param formData - The new form data to validate
539
581
  * @param schema - The schema used to validate against
540
- * @param altSchemaUtils - The alternate schemaUtils to use for validation
582
+ * @param [altSchemaUtils] - The alternate schemaUtils to use for validation
583
+ * @param [retrievedSchema] - An optionally retrieved schema for per
541
584
  */
542
585
  validate(
543
586
  formData: T | undefined,
544
- schema = this.props.schema,
587
+ schema = this.state.schema,
545
588
  altSchemaUtils?: SchemaUtilsType<T, S, F>,
546
589
  retrievedSchema?: S,
547
590
  ): ValidationData<T> {
@@ -556,7 +599,6 @@ export default class Form<
556
599
  /** Renders any errors contained in the `state` in using the `ErrorList`, if not disabled by `showErrorList`. */
557
600
  renderErrors(registry: Registry<T, S, F>) {
558
601
  const { errors, errorSchema, schema, uiSchema } = this.state;
559
- const { formContext } = this.props;
560
602
  const options = getUiOptions<T, S, F>(uiSchema);
561
603
  const ErrorListTemplate = getTemplate<'ErrorListTemplate', T, S, F>('ErrorListTemplate', registry, options);
562
604
 
@@ -567,7 +609,6 @@ export default class Form<
567
609
  errorSchema={errorSchema || {}}
568
610
  schema={schema}
569
611
  uiSchema={uiSchema}
570
- formContext={formContext}
571
612
  registry={registry}
572
613
  />
573
614
  );
@@ -575,6 +616,75 @@ export default class Form<
575
616
  return null;
576
617
  }
577
618
 
619
+ /** Merges any `extraErrors` or `customErrors` into the given `schemaValidation` object, returning the result
620
+ *
621
+ * @param schemaValidation - The `ValidationData` object into which additional errors are merged
622
+ * @param [extraErrors] - The extra errors from the props
623
+ * @param [customErrors] - The customErrors from custom components
624
+ * @return - The `extraErrors` and `customErrors` merged into the `schemaValidation`
625
+ * @private
626
+ */
627
+ private mergeErrors(
628
+ schemaValidation: ValidationData<T>,
629
+ extraErrors?: FormProps['extraErrors'],
630
+ customErrors?: ErrorSchemaBuilder,
631
+ ): ValidationData<T> {
632
+ let errorSchema: ErrorSchema<T> = schemaValidation.errorSchema;
633
+ let errors: RJSFValidationError[] = schemaValidation.errors;
634
+ if (extraErrors) {
635
+ const merged = validationDataMerge(schemaValidation, extraErrors);
636
+ errorSchema = merged.errorSchema;
637
+ errors = merged.errors;
638
+ }
639
+ if (customErrors) {
640
+ const merged = validationDataMerge(schemaValidation, customErrors.ErrorSchema, true);
641
+ errorSchema = merged.errorSchema;
642
+ errors = merged.errors;
643
+ }
644
+ return { errors, errorSchema };
645
+ }
646
+
647
+ /** Performs live validation and then updates and returns the errors and error schemas by potentially merging in
648
+ * `extraErrors` and `customErrors`.
649
+ *
650
+ * @param rootSchema - The `rootSchema` from the state
651
+ * @param schemaUtils - The `SchemaUtilsType` from the state
652
+ * @param originalErrorSchema - The original `ErrorSchema` from the state
653
+ * @param [formData] - The new form data to validate
654
+ * @param [extraErrors] - The extra errors from the props
655
+ * @param [customErrors] - The customErrors from custom components
656
+ * @param [retrievedSchema] - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`
657
+ * @param [mergeIntoOriginalErrorSchema=false] - Optional flag indicating whether we merge into original schema
658
+ * @returns - An object containing `errorSchema`, `errors`, `schemaValidationErrors` and `schemaValidationErrorSchema`
659
+ * @private
660
+ */
661
+ private liveValidate(
662
+ rootSchema: S,
663
+ schemaUtils: SchemaUtilsType<T, S, F>,
664
+ originalErrorSchema: ErrorSchema<S>,
665
+ formData?: T,
666
+ extraErrors?: FormProps['extraErrors'],
667
+ customErrors?: ErrorSchemaBuilder<T>,
668
+ retrievedSchema?: S,
669
+ mergeIntoOriginalErrorSchema = false,
670
+ ) {
671
+ const schemaValidation = this.validate(formData, rootSchema, schemaUtils, retrievedSchema);
672
+ const errors = schemaValidation.errors;
673
+ let errorSchema = schemaValidation.errorSchema;
674
+ // We merge 'originalErrorSchema' with 'schemaValidation.errorSchema.'; This done to display the raised field error.
675
+ if (mergeIntoOriginalErrorSchema) {
676
+ errorSchema = mergeObjects(
677
+ originalErrorSchema,
678
+ schemaValidation.errorSchema,
679
+ 'preventDuplicates',
680
+ ) as ErrorSchema<T>;
681
+ }
682
+ const schemaValidationErrors = errors;
683
+ const schemaValidationErrorSchema = errorSchema;
684
+ const mergedErrors = this.mergeErrors({ errorSchema, errors }, extraErrors, customErrors);
685
+ return { ...mergedErrors, schemaValidationErrors, schemaValidationErrorSchema };
686
+ }
687
+
578
688
  /** Returns the `formData` with only the elements specified in the `fields` list
579
689
  *
580
690
  * @param formData - The data for the `Form`
@@ -601,25 +711,28 @@ export default class Form<
601
711
  * @param [formData] - The form data to use while checking for empty objects/arrays
602
712
  */
603
713
  getFieldNames = (pathSchema: PathSchema<T>, formData?: T): string[][] => {
714
+ const formValueHasData = (value: T, isLeaf: boolean) =>
715
+ typeof value !== 'object' || _isEmpty(value) || (isLeaf && !_isEmpty(value));
604
716
  const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => {
605
- Object.keys(_obj).forEach((key: string) => {
606
- if (typeof _obj[key] === 'object') {
717
+ const objKeys = Object.keys(_obj);
718
+ objKeys.forEach((key: string) => {
719
+ const data = _obj[key];
720
+ if (typeof data === 'object') {
607
721
  const newPaths = paths.map((path) => [...path, key]);
608
722
  // 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]);
723
+ if (data[RJSF_ADDITIONAL_PROPERTIES_FLAG] && data[NAME_KEY] !== '') {
724
+ acc.push(data[NAME_KEY]);
611
725
  } else {
612
- getAllPaths(_obj[key], acc, newPaths);
726
+ getAllPaths(data, acc, newPaths);
613
727
  }
614
- } else if (key === NAME_KEY && _obj[key] !== '') {
728
+ } else if (key === NAME_KEY && data !== '') {
615
729
  paths.forEach((path) => {
616
730
  const formValue = _get(formData, path);
617
- // adds path to fieldNames if it points to a value
618
- // or an empty object/array
731
+ const isLeaf = objKeys.length === 1;
732
+ // adds path to fieldNames if it points to a value or an empty object/array which is not a leaf
619
733
  if (
620
- typeof formValue !== 'object' ||
621
- _isEmpty(formValue) ||
622
- (Array.isArray(formValue) && formValue.every((val) => typeof val !== 'object'))
734
+ formValueHasData(formValue, isLeaf) ||
735
+ (Array.isArray(formValue) && formValue.every((val) => formValueHasData(val, isLeaf)))
623
736
  ) {
624
737
  acc.push(path);
625
738
  }
@@ -642,74 +755,54 @@ export default class Form<
642
755
  const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
643
756
  const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData);
644
757
  const fieldNames = this.getFieldNames(pathSchema, formData);
645
- const newFormData = this.getUsedFormData(formData, fieldNames);
646
- return newFormData;
758
+ return this.getUsedFormData(formData, fieldNames);
647
759
  };
648
760
 
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;
761
+ /** Pushes the given change information into the `pendingChanges` array and then calls `processPendingChanges()` if
762
+ * the array only contains a single pending change.
763
+ *
764
+ * @param newValue - The new form data from a change to a field
765
+ * @param path - The path to the change into which to set the formData
766
+ * @param [newErrorSchema] - The new `ErrorSchema` based on the field change
767
+ * @param [id] - The id of the field that caused the change
768
+ */
769
+ onChange = (newValue: T | undefined, path: FieldPathList, newErrorSchema?: ErrorSchema<T>, id?: string) => {
770
+ this.pendingChanges.push({ newValue, path, newErrorSchema, id });
771
+ if (this.pendingChanges.length === 1) {
772
+ this.processPendingChange();
659
773
  }
774
+ };
660
775
 
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
- }
694
-
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.
701
- *
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
776
+ /** Function to handle changes made to a field in the `Form`. This handler gets the first change from the
777
+ * `pendingChanges` list, containing the `newValue` for the `formData` and the `path` at which the `newValue` is to be
778
+ * updated, along with a new, optional `ErrorSchema` for that same `path` and potentially the `id` of the field being
779
+ * changed. It will first update the `formData` with any missing default fields and then, if `omitExtraData` and
780
+ * `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not in a form field. Then, the
781
+ * resulting `formData` will be validated if required. The state will be updated with the new updated (potentially
782
+ * filtered) `formData`, any errors that resulted from validation. Finally the `onChange` callback will be called, if
783
+ * specified, with the updated state and the `processPendingChange()` function is called again.
705
784
  */
706
- onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema<T>, id?: string) => {
785
+ processPendingChange() {
786
+ if (this.pendingChanges.length === 0) {
787
+ return;
788
+ }
789
+ const { newValue, path, id } = this.pendingChanges[0];
790
+ const { newErrorSchema } = this.pendingChanges[0];
707
791
  const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
708
- const { schemaUtils, schema } = this.state;
792
+ const { formData: oldFormData, schemaUtils, schema, fieldPathId, schemaValidationErrorSchema, errors } = this.state;
793
+ let { customErrors, errorSchema: originalErrorSchema } = this.state;
794
+ const rootPathId = fieldPathId.path[0] || '';
709
795
 
796
+ const isRootPath = !path || path.length === 0 || (path.length === 1 && path[0] === rootPathId);
710
797
  let retrievedSchema = this.state.retrievedSchema;
798
+ let formData = isRootPath ? newValue : _cloneDeep(oldFormData);
711
799
  if (isObject(formData) || Array.isArray(formData)) {
712
- const newState = this.getStateFromProps(this.props, formData);
800
+ if (!isRootPath) {
801
+ // If the newValue is not on the root path, then set it into the form data
802
+ _set(formData, path, newValue);
803
+ }
804
+ // Pass true to skip live validation in `getStateFromProps()` since we will do it a bit later
805
+ const newState = this.getStateFromProps(this.props, formData, undefined, undefined, undefined, true);
713
806
  formData = newState.formData;
714
807
  retrievedSchema = newState.retrievedSchema;
715
808
  }
@@ -725,41 +818,62 @@ export default class Form<
725
818
  };
726
819
  }
727
820
 
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>;
821
+ if (newErrorSchema) {
822
+ // First check to see if there is an existing validation error on this path...
823
+ // @ts-expect-error TS2590, because getting from the error schema is confusing TS
824
+ const oldValidationError = !isRootPath ? _get(schemaValidationErrorSchema, path) : schemaValidationErrorSchema;
825
+ // If there is an old validation error for this path, assume we are updating it directly
826
+ if (!_isEmpty(oldValidationError)) {
827
+ // Update the originalErrorSchema "in place" or replace it if it is the root
828
+ if (!isRootPath) {
829
+ _set(originalErrorSchema, path, newErrorSchema);
830
+ } else {
831
+ originalErrorSchema = newErrorSchema;
832
+ }
833
+ } else {
834
+ if (!customErrors) {
835
+ customErrors = new ErrorSchemaBuilder<T>();
836
+ }
837
+ if (isRootPath) {
838
+ customErrors.setErrors(_get(newErrorSchema, ERRORS_KEY, ''));
839
+ } else {
840
+ _set(customErrors.ErrorSchema, path, newErrorSchema);
841
+ }
743
842
  }
744
- state = {
745
- formData: newFormData,
746
- errors,
747
- errorSchema,
748
- schemaValidationErrors,
749
- schemaValidationErrorSchema,
750
- };
843
+ } else if (customErrors && _get(customErrors.ErrorSchema, [...path, ERRORS_KEY])) {
844
+ // If we have custom errors and the path has an error, then we need to clear it
845
+ customErrors.clearErrors(path);
846
+ }
847
+ // If there are pending changes in the queue, skip live validation since it will happen with the last change
848
+ if (mustValidate && this.pendingChanges.length === 1) {
849
+ const liveValidation = this.liveValidate(
850
+ schema,
851
+ schemaUtils,
852
+ originalErrorSchema,
853
+ newFormData,
854
+ extraErrors,
855
+ customErrors,
856
+ retrievedSchema,
857
+ );
858
+ state = { formData: newFormData, ...liveValidation, customErrors };
751
859
  } else if (!noValidate && newErrorSchema) {
752
- const errorSchema = extraErrors
753
- ? (mergeObjects(newErrorSchema, extraErrors, 'preventDuplicates') as ErrorSchema<T>)
754
- : newErrorSchema;
860
+ // Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
861
+ const mergedErrors = this.mergeErrors({ errorSchema: originalErrorSchema, errors }, extraErrors, customErrors);
755
862
  state = {
756
863
  formData: newFormData,
757
- errorSchema: errorSchema,
758
- errors: toErrorList(errorSchema),
864
+ ...mergedErrors,
865
+ customErrors,
759
866
  };
760
867
  }
761
- this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id));
762
- };
868
+ this.setState(state as FormState<T, S, F>, () => {
869
+ if (onChange) {
870
+ onChange({ ...this.state, ...state }, id);
871
+ }
872
+ // Now remove the change we just completed and call this again
873
+ this.pendingChanges.shift();
874
+ this.processPendingChange();
875
+ });
876
+ }
763
877
 
764
878
  /**
765
879
  * If the retrievedSchema has changed the new retrievedSchema is returned.
@@ -791,6 +905,8 @@ export default class Form<
791
905
  errors: [] as unknown,
792
906
  schemaValidationErrors: [] as unknown,
793
907
  schemaValidationErrorSchema: {},
908
+ initialDefaultsGenerated: false,
909
+ customErrors: undefined,
794
910
  } as FormState<T, S, F>;
795
911
 
796
912
  this.setState(state, () => onChange && onChange({ ...this.state, ...state }));
@@ -866,10 +982,28 @@ export default class Form<
866
982
  }
867
983
  };
868
984
 
985
+ /** Extracts the `GlobalFormOptions` from the given Form `props`
986
+ *
987
+ * @param props - The form props to extract the global form options from
988
+ * @returns - The `GlobalFormOptions` from the props
989
+ * @private
990
+ */
991
+ private getGlobalFormOptions(props: FormProps<T, S, F>): GlobalFormOptions {
992
+ const {
993
+ uiSchema = {},
994
+ experimental_componentUpdateStrategy,
995
+ idSeparator = DEFAULT_ID_SEPARATOR,
996
+ idPrefix = DEFAULT_ID_PREFIX,
997
+ } = props;
998
+ const rootFieldId = uiSchema['ui:rootFieldId'];
999
+ // Omit any options that are undefined or null
1000
+ return { idPrefix: rootFieldId || idPrefix, idSeparator, experimental_componentUpdateStrategy };
1001
+ }
1002
+
869
1003
  /** Returns the registry for the form */
870
1004
  getRegistry(): Registry<T, S, F> {
871
1005
  const { translateString: customTranslateString, uiSchema = {} } = this.props;
872
- const { schemaUtils } = this.state;
1006
+ const { schema, schemaUtils } = this.state;
873
1007
  const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry<T, S, F>();
874
1008
  return {
875
1009
  fields: { ...fields, ...this.props.fields },
@@ -882,11 +1016,12 @@ export default class Form<
882
1016
  },
883
1017
  },
884
1018
  widgets: { ...widgets, ...this.props.widgets },
885
- rootSchema: this.props.schema,
1019
+ rootSchema: schema,
886
1020
  formContext: this.props.formContext || formContext,
887
1021
  schemaUtils,
888
1022
  translateString: customTranslateString || translateString,
889
1023
  globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
1024
+ globalFormOptions: this.getGlobalFormOptions(this.props),
890
1025
  };
891
1026
  }
892
1027
 
@@ -1011,8 +1146,6 @@ export default class Form<
1011
1146
  const {
1012
1147
  children,
1013
1148
  id,
1014
- idPrefix,
1015
- idSeparator,
1016
1149
  className = '',
1017
1150
  tagName,
1018
1151
  name,
@@ -1025,12 +1158,11 @@ export default class Form<
1025
1158
  noHtml5Validate = false,
1026
1159
  disabled,
1027
1160
  readonly,
1028
- formContext,
1029
1161
  showErrorList = 'top',
1030
1162
  _internalFormWrapper,
1031
1163
  } = this.props;
1032
1164
 
1033
- const { schema, uiSchema, formData, errorSchema, idSchema } = this.state;
1165
+ const { schema, uiSchema, formData, errorSchema, fieldPathId } = this.state;
1034
1166
  const registry = this.getRegistry();
1035
1167
  const { SchemaField: _SchemaField } = registry.fields;
1036
1168
  const { SubmitButton } = registry.templates.ButtonTemplates;
@@ -1068,10 +1200,7 @@ export default class Form<
1068
1200
  schema={schema}
1069
1201
  uiSchema={uiSchema}
1070
1202
  errorSchema={errorSchema}
1071
- idSchema={idSchema}
1072
- idPrefix={idPrefix}
1073
- idSeparator={idSeparator}
1074
- formContext={formContext}
1203
+ fieldPathId={fieldPathId}
1075
1204
  formData={formData}
1076
1205
  onChange={this.onChange}
1077
1206
  onBlur={this.onBlur}