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

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 (93) hide show
  1. package/dist/core.umd.js +469 -360
  2. package/dist/{index.js → index.cjs} +640 -519
  3. package/dist/index.cjs.map +7 -0
  4. package/dist/index.esm.js +706 -566
  5. package/dist/index.esm.js.map +4 -4
  6. package/lib/components/Form.d.ts +66 -16
  7. package/lib/components/Form.d.ts.map +1 -1
  8. package/lib/components/Form.js +138 -59
  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 +92 -59
  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 -53
  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 +18 -25
  28. package/lib/components/fields/SchemaField.d.ts.map +1 -1
  29. package/lib/components/fields/SchemaField.js +17 -17
  30. package/lib/components/fields/StringField.d.ts.map +1 -1
  31. package/lib/components/fields/StringField.js +7 -2
  32. package/lib/components/templates/ArrayFieldDescriptionTemplate.d.ts +1 -1
  33. package/lib/components/templates/ArrayFieldDescriptionTemplate.js +3 -3
  34. package/lib/components/templates/ArrayFieldItemButtonsTemplate.js +2 -2
  35. package/lib/components/templates/ArrayFieldTemplate.js +3 -3
  36. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts +1 -1
  37. package/lib/components/templates/ArrayFieldTitleTemplate.js +3 -3
  38. package/lib/components/templates/FieldErrorTemplate.js +2 -2
  39. package/lib/components/templates/FieldHelpTemplate.js +2 -2
  40. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts +8 -0
  41. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts.map +1 -0
  42. package/lib/components/templates/MultiSchemaFieldTemplate.js +10 -0
  43. package/lib/components/templates/ObjectFieldTemplate.js +2 -2
  44. package/lib/components/templates/UnsupportedField.js +3 -3
  45. package/lib/components/templates/index.d.ts.map +1 -1
  46. package/lib/components/templates/index.js +2 -0
  47. package/lib/components/widgets/AltDateWidget.d.ts.map +1 -1
  48. package/lib/components/widgets/AltDateWidget.js +15 -18
  49. package/lib/components/widgets/CheckboxesWidget.js +2 -2
  50. package/lib/getDefaultRegistry.d.ts.map +1 -1
  51. package/lib/getDefaultRegistry.js +2 -1
  52. package/lib/getTestRegistry.d.ts +5 -0
  53. package/lib/getTestRegistry.d.ts.map +1 -0
  54. package/lib/getTestRegistry.js +19 -0
  55. package/lib/index.d.ts +2 -1
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +2 -1
  58. package/lib/tsconfig.tsbuildinfo +1 -1
  59. package/package.json +18 -19
  60. package/src/components/Form.tsx +183 -73
  61. package/src/components/fields/ArrayField.tsx +99 -67
  62. package/src/components/fields/BooleanField.tsx +12 -3
  63. package/src/components/fields/LayoutGridField.tsx +95 -82
  64. package/src/components/fields/LayoutHeaderField.tsx +3 -3
  65. package/src/components/fields/LayoutMultiSchemaField.tsx +5 -5
  66. package/src/components/fields/MultiSchemaField.tsx +51 -35
  67. package/src/components/fields/NullField.tsx +3 -3
  68. package/src/components/fields/NumberField.tsx +11 -3
  69. package/src/components/fields/ObjectField.tsx +19 -36
  70. package/src/components/fields/SchemaField.tsx +24 -30
  71. package/src/components/fields/StringField.tsx +12 -3
  72. package/src/components/templates/ArrayFieldDescriptionTemplate.tsx +3 -3
  73. package/src/components/templates/ArrayFieldItemButtonsTemplate.tsx +5 -5
  74. package/src/components/templates/ArrayFieldTemplate.tsx +5 -5
  75. package/src/components/templates/ArrayFieldTitleTemplate.tsx +3 -3
  76. package/src/components/templates/BaseInputTemplate.tsx +3 -3
  77. package/src/components/templates/FieldErrorTemplate.tsx +2 -2
  78. package/src/components/templates/FieldHelpTemplate.tsx +2 -2
  79. package/src/components/templates/MultiSchemaFieldTemplate.tsx +20 -0
  80. package/src/components/templates/ObjectFieldTemplate.tsx +5 -5
  81. package/src/components/templates/UnsupportedField.tsx +3 -3
  82. package/src/components/templates/WrapIfAdditionalTemplate.tsx +1 -1
  83. package/src/components/templates/index.ts +2 -0
  84. package/src/components/widgets/AltDateWidget.tsx +21 -23
  85. package/src/components/widgets/CheckboxWidget.tsx +2 -2
  86. package/src/components/widgets/CheckboxesWidget.tsx +3 -3
  87. package/src/components/widgets/RadioWidget.tsx +1 -1
  88. package/src/components/widgets/SelectWidget.tsx +1 -1
  89. package/src/components/widgets/TextareaWidget.tsx +1 -1
  90. package/src/getDefaultRegistry.ts +10 -1
  91. package/src/getTestRegistry.tsx +34 -0
  92. package/src/index.ts +2 -1
  93. package/dist/index.js.map +0 -7
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@rjsf/core",
3
- "version": "6.0.0-beta.2",
3
+ "version": "6.0.0-beta.20",
4
4
  "description": "A simple React component capable of building HTML forms out of a JSON schema.",
5
5
  "scripts": {
6
6
  "compileReplacer": "tsc -p tsconfig.replacer.json && move-file lodashReplacer.js lodashReplacer.cjs",
7
7
  "build:ts": "npm run compileReplacer && rimraf ./lib && tsc -b tsconfig.build.json && tsc-alias -p tsconfig.build.json",
8
- "build:cjs": "esbuild ./src/index.ts --bundle --outfile=dist/index.js --sourcemap --packages=external --format=cjs",
8
+ "build:cjs": "esbuild ./src/index.ts --bundle --outfile=dist/index.cjs --sourcemap --packages=external --format=cjs",
9
9
  "build:esm": "esbuild ./src/index.ts --bundle --outfile=dist/index.esm.js --sourcemap --packages=external --format=esm",
10
10
  "build:umd": "rollup dist/index.esm.js --format=umd --file=dist/core.umd.js --name=JSONSchemaForm",
11
11
  "build": "npm run build:ts && npm run build:cjs && npm run build:esm && npm run build:umd",
@@ -32,27 +32,27 @@
32
32
  "exports": {
33
33
  ".": {
34
34
  "types": "./lib/index.d.ts",
35
- "require": "./dist/index.js",
35
+ "require": "./dist/index.cjs",
36
36
  "import": "./lib/index.js"
37
37
  },
38
38
  "./lib": {
39
39
  "types": "./lib/index.d.ts",
40
- "require": "./dist/index.js",
40
+ "require": "./dist/index.cjs",
41
41
  "import": "./lib/index.js"
42
42
  },
43
43
  "./lib/*.js": {
44
44
  "types": "./lib/*.d.ts",
45
- "require": "./dist/*.js",
45
+ "require": "./dist/*.cjs",
46
46
  "import": "./lib/*.js"
47
47
  },
48
48
  "./dist": {
49
49
  "types": "./lib/index.d.ts",
50
- "require": "./dist/index.js",
50
+ "require": "./dist/index.cjs",
51
51
  "import": "./lib/index.js"
52
52
  },
53
- "./dist/*.js": {
53
+ "./dist/*.cjs": {
54
54
  "types": "./lib/*.d.ts",
55
- "require": "./dist/*.js",
55
+ "require": "./dist/*.cjs",
56
56
  "import": "./lib/*.js"
57
57
  }
58
58
  },
@@ -66,30 +66,29 @@
66
66
  "node": ">=20"
67
67
  },
68
68
  "peerDependencies": {
69
- "@rjsf/utils": "^6.0.0-beta.x",
69
+ "@rjsf/utils": "^6.0.0-beta.20",
70
70
  "react": ">=18"
71
71
  },
72
72
  "dependencies": {
73
73
  "lodash": "^4.17.21",
74
74
  "lodash-es": "^4.17.21",
75
- "markdown-to-jsx": "^7.7.6",
76
- "nanoid": "^5.1.5",
75
+ "markdown-to-jsx": "^7.7.13",
77
76
  "prop-types": "^15.8.1"
78
77
  },
79
78
  "devDependencies": {
80
- "@rjsf/snapshot-tests": "^6.0.0-beta.2",
81
- "@rjsf/utils": "^6.0.0-beta.2",
82
- "@rjsf/validator-ajv8": "^6.0.0-beta.2",
83
- "@testing-library/jest-dom": "^6.6.3",
84
- "@testing-library/react": "^16.2.0",
79
+ "@rjsf/snapshot-tests": "^6.0.0-beta.20",
80
+ "@rjsf/utils": "^6.0.0-beta.20",
81
+ "@rjsf/validator-ajv8": "^6.0.0-beta.20",
82
+ "@testing-library/jest-dom": "^6.9.1",
83
+ "@testing-library/react": "^16.3.0",
85
84
  "@testing-library/user-event": "^14.6.1",
86
- "ajv": "^8.12.0",
85
+ "ajv": "^8.17.1",
87
86
  "atob": "^2.1.2",
88
87
  "chai": "^3.5.0",
89
- "eslint": "^8.56.0",
88
+ "eslint": "^8.57.1",
90
89
  "html": "^1.0.0",
91
90
  "jsdom": "^20.0.3",
92
- "mocha": "^10.2.0",
91
+ "mocha": "^10.8.2",
93
92
  "react-portal": "^4.3.0",
94
93
  "sinon": "^9.2.4"
95
94
  },
@@ -3,14 +3,16 @@ import {
3
3
  createSchemaUtils,
4
4
  CustomValidator,
5
5
  deepEquals,
6
+ ERRORS_KEY,
6
7
  ErrorSchema,
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,
@@ -37,12 +40,17 @@ import {
37
40
  Experimental_CustomMergeAllOf,
38
41
  createErrorHandler,
39
42
  unwrapErrorHandler,
43
+ DEFAULT_ID_SEPARATOR,
44
+ DEFAULT_ID_PREFIX,
45
+ GlobalFormOptions,
40
46
  } from '@rjsf/utils';
47
+ import _cloneDeep from 'lodash/cloneDeep';
41
48
  import _forEach from 'lodash/forEach';
42
49
  import _get from 'lodash/get';
43
50
  import _isEmpty from 'lodash/isEmpty';
44
51
  import _isNil from 'lodash/isNil';
45
52
  import _pick from 'lodash/pick';
53
+ import _set from 'lodash/set';
46
54
  import _toPath from 'lodash/toPath';
47
55
 
48
56
  import getDefaultRegistry from '../getDefaultRegistry';
@@ -195,8 +203,20 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
195
203
  * `emptyObjectFields`
196
204
  */
197
205
  experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
206
+ /**
207
+ * Controls the component update strategy used by the Form's `shouldComponentUpdate` lifecycle method.
208
+ *
209
+ * - `'customDeep'`: Uses RJSF's custom deep equality checks via the `deepEquals` utility function,
210
+ * which treats all functions as equivalent and provides optimized performance for form data comparisons.
211
+ * - `'shallow'`: Uses shallow comparison of props and state (only compares direct properties). This matches React's PureComponent behavior.
212
+ * - `'always'`: Always rerenders when called. This matches React's Component behavior.
213
+ *
214
+ * @default 'customDeep'
215
+ */
216
+ experimental_componentUpdateStrategy?: 'customDeep' | 'shallow' | 'always';
198
217
  /** Optional function that allows for custom merging of `allOf` schemas
199
218
  */
219
+
200
220
  experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
201
221
  // Private
202
222
  /**
@@ -226,10 +246,10 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
226
246
  schema: S;
227
247
  /** The uiSchema for the form */
228
248
  uiSchema: UiSchema<T, S, F>;
229
- /** The `IdSchema` for the form, computed from the `schema`, the `rootFieldId`, the `formData` and the `idPrefix` and
249
+ /** The `FieldPathId` for the form, computed from the `schema`, the `rootFieldId`, the `idPrefix` and
230
250
  * `idSeparator` props.
231
251
  */
232
- idSchema: IdSchema<T>;
252
+ fieldPathId: FieldPathId;
233
253
  /** The schemaUtils implementation used by the `Form`, created from the `validator` and the `schema` */
234
254
  schemaUtils: SchemaUtilsType<T, S, F>;
235
255
  /** The current data for the form, computed from the `formData` prop and the changes made by the user */
@@ -260,6 +280,19 @@ export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema,
260
280
  status?: 'submitted';
261
281
  }
262
282
 
283
+ /** The definition of a pending change that will be processed in the `onChange` handler
284
+ */
285
+ interface PendingChange<T> {
286
+ /** The path into the formData/errorSchema at which the `newValue`/`newErrorSchema` will be set */
287
+ path: FieldPathList;
288
+ /** The new value to set into the formData */
289
+ newValue?: T;
290
+ /** The new errors to be set into the errorSchema, if any */
291
+ newErrorSchema?: ErrorSchema<T>;
292
+ /** The optional id of the field for which the change is being made */
293
+ id?: string;
294
+ }
295
+
263
296
  /** The `Form` component renders the outer form and all the fields defined in the `schema` */
264
297
  export default class Form<
265
298
  T = any,
@@ -271,6 +304,10 @@ export default class Form<
271
304
  */
272
305
  formElement: RefObject<any>;
273
306
 
307
+ /** The list of pending changes
308
+ */
309
+ pendingChanges: PendingChange<T>[] = [];
310
+
274
311
  /** Constructs the `Form` from the `props`. Will setup the initial state from the props. It will also call the
275
312
  * `onChange` handler if the initially provided `formData` is modified to add missing default values as part of the
276
313
  * state construction.
@@ -314,12 +351,18 @@ export default class Form<
314
351
  prevState: FormState<T, S, F>,
315
352
  ): { nextState: FormState<T, S, F>; shouldUpdate: true } | { shouldUpdate: false } {
316
353
  if (!deepEquals(this.props, prevProps)) {
354
+ // Compare the previous props formData against the current props formData
317
355
  const formDataChangedFields = getChangedFields(this.props.formData, prevProps.formData);
356
+ // Compare the current props formData against the current state's formData to determine if the new props were the
357
+ // result of the onChange from the existing state formData
358
+ const stateDataChangedFields = getChangedFields(this.props.formData, this.state.formData);
318
359
  const isSchemaChanged = !deepEquals(prevProps.schema, this.props.schema);
319
360
  // When formData is not an object, getChangedFields returns an empty array.
320
361
  // In this case, deepEquals is most needed to check again.
321
362
  const isFormDataChanged =
322
363
  formDataChangedFields.length > 0 || !deepEquals(prevProps.formData, this.props.formData);
364
+ const isStateDataChanged =
365
+ stateDataChangedFields.length > 0 || !deepEquals(this.state.formData, this.props.formData);
323
366
  const nextState = this.getStateFromProps(
324
367
  this.props,
325
368
  this.props.formData,
@@ -329,6 +372,8 @@ export default class Form<
329
372
  isSchemaChanged || isFormDataChanged ? undefined : this.state.retrievedSchema,
330
373
  isSchemaChanged,
331
374
  formDataChangedFields,
375
+ // Skip live validation for this request if no form data has changed from the last state
376
+ !isStateDataChanged,
332
377
  );
333
378
  const shouldUpdate = !deepEquals(nextState, prevState);
334
379
  return { nextState, shouldUpdate };
@@ -376,6 +421,7 @@ export default class Form<
376
421
  * @param retrievedSchema - An expanded schema, if not provided, it will be retrieved from the `schema` and `formData`.
377
422
  * @param isSchemaChanged - A flag indicating whether the schema has changed.
378
423
  * @param formDataChangedFields - The changed fields of `formData`
424
+ * @param skipLiveValidate - Optional flag, if true, means that we are not running live validation
379
425
  * @returns - The new state for the `Form`
380
426
  */
381
427
  getStateFromProps(
@@ -384,14 +430,15 @@ export default class Form<
384
430
  retrievedSchema?: S,
385
431
  isSchemaChanged = false,
386
432
  formDataChangedFields: string[] = [],
433
+ skipLiveValidate = false,
387
434
  ): FormState<T, S, F> {
388
435
  const state: FormState<T, S, F> = this.state || {};
389
436
  const schema = 'schema' in props ? props.schema : this.props.schema;
437
+ const validator = 'validator' in props ? props.validator : this.props.validator;
390
438
  const uiSchema: UiSchema<T, S, F> = ('uiSchema' in props ? props.uiSchema! : this.props.uiSchema!) || {};
391
439
  const edit = typeof inputFormData !== 'undefined';
392
440
  const liveValidate = 'liveValidate' in props ? props.liveValidate : this.props.liveValidate;
393
441
  const mustValidate = edit && !props.noValidate && liveValidate;
394
- const rootSchema = schema;
395
442
  const experimental_defaultFormStateBehavior =
396
443
  'experimental_defaultFormStateBehavior' in props
397
444
  ? props.experimental_defaultFormStateBehavior
@@ -404,22 +451,23 @@ export default class Form<
404
451
  if (
405
452
  !schemaUtils ||
406
453
  schemaUtils.doesSchemaUtilsDiffer(
407
- props.validator,
408
- rootSchema,
454
+ validator,
455
+ schema,
409
456
  experimental_defaultFormStateBehavior,
410
457
  experimental_customMergeAllOf,
411
458
  )
412
459
  ) {
413
460
  schemaUtils = createSchemaUtils<T, S, F>(
414
- props.validator,
415
- rootSchema,
461
+ validator,
462
+ schema,
416
463
  experimental_defaultFormStateBehavior,
417
464
  experimental_customMergeAllOf,
418
465
  );
419
466
  }
420
- const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;
467
+ const rootSchema = schemaUtils.getRootSchema();
468
+ const formData: T = schemaUtils.getDefaultFormState(rootSchema, inputFormData) as T;
421
469
  const _retrievedSchema = this.updateRetrievedSchema(
422
- retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData),
470
+ retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData),
423
471
  );
424
472
 
425
473
  const getCurrentErrors = (): ValidationData<T> => {
@@ -442,8 +490,9 @@ export default class Form<
442
490
  let errorSchema: ErrorSchema<T> | undefined;
443
491
  let schemaValidationErrors: RJSFValidationError[] = state.schemaValidationErrors;
444
492
  let schemaValidationErrorSchema: ErrorSchema<T> = state.schemaValidationErrorSchema;
445
- if (mustValidate) {
446
- const schemaValidation = this.validate(formData, schema, schemaUtils, _retrievedSchema);
493
+ // If we are skipping live validate, it means that the state has already been updated with live validation errors
494
+ if (mustValidate && !skipLiveValidate) {
495
+ const schemaValidation = this.validate(formData, rootSchema, schemaUtils, _retrievedSchema);
447
496
  errors = schemaValidation.errors;
448
497
  // If retrievedSchema is undefined which means the schema or formData has changed, we do not merge state.
449
498
  // Else in the case where it hasn't changed, we merge 'state.errorSchema' with 'schemaValidation.errorSchema.' This done to display the raised field error.
@@ -462,7 +511,8 @@ export default class Form<
462
511
  const currentErrors = getCurrentErrors();
463
512
  errors = currentErrors.errors;
464
513
  errorSchema = currentErrors.errorSchema;
465
- if (formDataChangedFields.length > 0) {
514
+ // We only update the error schema for changed fields if mustValidate is false
515
+ if (formDataChangedFields.length > 0 && !mustValidate) {
466
516
  const newErrorSchema = formDataChangedFields.reduce(
467
517
  (acc, key) => {
468
518
  acc[key] = undefined;
@@ -483,18 +533,12 @@ export default class Form<
483
533
  errorSchema = merged.errorSchema;
484
534
  errors = merged.errors;
485
535
  }
486
- const idSchema = schemaUtils.toIdSchema(
487
- _retrievedSchema,
488
- uiSchema['ui:rootFieldId'],
489
- formData,
490
- props.idPrefix,
491
- props.idSeparator,
492
- );
536
+ const fieldPathId = toFieldPathId('', this.getGlobalFormOptions(this.props));
493
537
  const nextState: FormState<T, S, F> = {
494
538
  schemaUtils,
495
- schema,
539
+ schema: rootSchema,
496
540
  uiSchema,
497
- idSchema,
541
+ fieldPathId,
498
542
  formData,
499
543
  edit,
500
544
  errors,
@@ -513,9 +557,9 @@ export default class Form<
513
557
  * @returns - True if the component should be updated, false otherwise
514
558
  */
515
559
  shouldComponentUpdate(nextProps: FormProps<T, S, F>, nextState: FormState<T, S, F>): boolean {
516
- return shouldRender(this, nextProps, nextState);
560
+ const { experimental_componentUpdateStrategy = 'customDeep' } = this.props;
561
+ return shouldRender(this, nextProps, nextState, experimental_componentUpdateStrategy);
517
562
  }
518
-
519
563
  /** Gets the previously raised customValidate errors.
520
564
  *
521
565
  * @returns the previous customValidate errors
@@ -526,8 +570,7 @@ export default class Form<
526
570
  let customValidateErrors = {};
527
571
  if (typeof customValidate === 'function') {
528
572
  const errorHandler = customValidate(prevFormData, createErrorHandler<T>(prevFormData), uiSchema);
529
- const userErrorSchema = unwrapErrorHandler<T>(errorHandler);
530
- customValidateErrors = userErrorSchema;
573
+ customValidateErrors = unwrapErrorHandler<T>(errorHandler);
531
574
  }
532
575
  return customValidateErrors;
533
576
  }
@@ -537,11 +580,12 @@ export default class Form<
537
580
  *
538
581
  * @param formData - The new form data to validate
539
582
  * @param schema - The schema used to validate against
540
- * @param altSchemaUtils - The alternate schemaUtils to use for validation
583
+ * @param [altSchemaUtils] - The alternate schemaUtils to use for validation
584
+ * @param [retrievedSchema] - An optionally retrieved schema for per
541
585
  */
542
586
  validate(
543
587
  formData: T | undefined,
544
- schema = this.props.schema,
588
+ schema = this.state.schema,
545
589
  altSchemaUtils?: SchemaUtilsType<T, S, F>,
546
590
  retrievedSchema?: S,
547
591
  ): ValidationData<T> {
@@ -556,7 +600,6 @@ export default class Form<
556
600
  /** Renders any errors contained in the `state` in using the `ErrorList`, if not disabled by `showErrorList`. */
557
601
  renderErrors(registry: Registry<T, S, F>) {
558
602
  const { errors, errorSchema, schema, uiSchema } = this.state;
559
- const { formContext } = this.props;
560
603
  const options = getUiOptions<T, S, F>(uiSchema);
561
604
  const ErrorListTemplate = getTemplate<'ErrorListTemplate', T, S, F>('ErrorListTemplate', registry, options);
562
605
 
@@ -567,7 +610,6 @@ export default class Form<
567
610
  errorSchema={errorSchema || {}}
568
611
  schema={schema}
569
612
  uiSchema={uiSchema}
570
- formContext={formContext}
571
613
  registry={registry}
572
614
  />
573
615
  );
@@ -601,25 +643,28 @@ export default class Form<
601
643
  * @param [formData] - The form data to use while checking for empty objects/arrays
602
644
  */
603
645
  getFieldNames = (pathSchema: PathSchema<T>, formData?: T): string[][] => {
646
+ const formValueHasData = (value: T, isLeaf: boolean) =>
647
+ typeof value !== 'object' || _isEmpty(value) || (isLeaf && !_isEmpty(value));
604
648
  const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => {
605
- Object.keys(_obj).forEach((key: string) => {
606
- if (typeof _obj[key] === 'object') {
649
+ const objKeys = Object.keys(_obj);
650
+ objKeys.forEach((key: string) => {
651
+ const data = _obj[key];
652
+ if (typeof data === 'object') {
607
653
  const newPaths = paths.map((path) => [...path, key]);
608
654
  // 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]);
655
+ if (data[RJSF_ADDITIONAL_PROPERTIES_FLAG] && data[NAME_KEY] !== '') {
656
+ acc.push(data[NAME_KEY]);
611
657
  } else {
612
- getAllPaths(_obj[key], acc, newPaths);
658
+ getAllPaths(data, acc, newPaths);
613
659
  }
614
- } else if (key === NAME_KEY && _obj[key] !== '') {
660
+ } else if (key === NAME_KEY && data !== '') {
615
661
  paths.forEach((path) => {
616
662
  const formValue = _get(formData, path);
617
- // adds path to fieldNames if it points to a value
618
- // or an empty object/array
663
+ const isLeaf = objKeys.length === 1;
664
+ // adds path to fieldNames if it points to a value or an empty object/array which is not a leaf
619
665
  if (
620
- typeof formValue !== 'object' ||
621
- _isEmpty(formValue) ||
622
- (Array.isArray(formValue) && formValue.every((val) => typeof val !== 'object'))
666
+ formValueHasData(formValue, isLeaf) ||
667
+ (Array.isArray(formValue) && formValue.every((val) => formValueHasData(val, isLeaf)))
623
668
  ) {
624
669
  acc.push(path);
625
670
  }
@@ -642,11 +687,16 @@ export default class Form<
642
687
  const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
643
688
  const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData);
644
689
  const fieldNames = this.getFieldNames(pathSchema, formData);
645
- const newFormData = this.getUsedFormData(formData, fieldNames);
646
- return newFormData;
690
+ return this.getUsedFormData(formData, fieldNames);
647
691
  };
648
692
 
649
- // Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
693
+ /** Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
694
+ *
695
+ * @param schemaErrors - The schema errors to filter
696
+ * @param [resolvedSchema] - An optionally resolved schema to use for performance reasons
697
+ * @param [formData] - The formData to help filter errors
698
+ * @private
699
+ */
650
700
  private filterErrorsBasedOnSchema(schemaErrors: ErrorSchema<T>, resolvedSchema?: S, formData?: any): ErrorSchema<T> {
651
701
  const { retrievedSchema, schemaUtils } = this.state;
652
702
  const _retrievedSchema = resolvedSchema ?? retrievedSchema;
@@ -655,7 +705,7 @@ export default class Form<
655
705
  const filteredErrors: ErrorSchema<T> = _pick(schemaErrors, fieldNames as unknown as string[]);
656
706
  // If the root schema is of a primitive type, do not filter out the __errors
657
707
  if (resolvedSchema?.type !== 'object' && resolvedSchema?.type !== 'array') {
658
- filteredErrors.__errors = schemaErrors.__errors;
708
+ filteredErrors[ERRORS_KEY] = schemaErrors[ERRORS_KEY];
659
709
  }
660
710
 
661
711
  const prevCustomValidateErrors = this.getPreviousCustomValidateErrors();
@@ -679,11 +729,16 @@ export default class Form<
679
729
  } else if (
680
730
  isObject(errorAtKey) &&
681
731
  isObject(prevCustomValidateErrorAtKey) &&
682
- Array.isArray(prevCustomValidateErrorAtKey?.__errors)
732
+ Array.isArray(prevCustomValidateErrorAtKey?.[ERRORS_KEY])
683
733
  ) {
684
734
  // 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)) {
735
+ errors[errorKey] = {
736
+ [ERRORS_KEY]: filterPreviousCustomErrors(
737
+ errorAtKey[ERRORS_KEY],
738
+ prevCustomValidateErrorAtKey?.[ERRORS_KEY],
739
+ ),
740
+ };
741
+ } else if (typeof errorAtKey === 'object' && !Array.isArray(errorAtKey[ERRORS_KEY])) {
687
742
  filterNilOrEmptyErrors(errorAtKey, previousCustomValidateErrors[errorKey]);
688
743
  }
689
744
  });
@@ -692,24 +747,50 @@ export default class Form<
692
747
  return filterNilOrEmptyErrors(filteredErrors, prevCustomValidateErrors);
693
748
  }
694
749
 
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.
750
+ /** Pushes the given change information into the `pendingChanges` array and then calls `processPendingChanges()` if
751
+ * the array only contains a single pending change.
701
752
  *
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
753
+ * @param newValue - The new form data from a change to a field
754
+ * @param path - The path to the change into which to set the formData
755
+ * @param [newErrorSchema] - The new `ErrorSchema` based on the field change
756
+ * @param [id] - The id of the field that caused the change
757
+ */
758
+ onChange = (newValue: T | undefined, path: FieldPathList, newErrorSchema?: ErrorSchema<T>, id?: string) => {
759
+ this.pendingChanges.push({ newValue, path, newErrorSchema, id });
760
+ if (this.pendingChanges.length === 1) {
761
+ this.processPendingChange();
762
+ }
763
+ };
764
+
765
+ /** Function to handle changes made to a field in the `Form`. This handler gets the first change from the
766
+ * `pendingChanges` list, containing the `newValue` for the `formData` and the `path` at which the `newValue` is to be
767
+ * updated, along with a new, optional `ErrorSchema` for that same `path` and potentially the `id` of the field being
768
+ * changed. It will first update the `formData` with any missing default fields and then, if `omitExtraData` and
769
+ * `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not in a form field. Then, the
770
+ * resulting `formData` will be validated if required. The state will be updated with the new updated (potentially
771
+ * filtered) `formData`, any errors that resulted from validation. Finally the `onChange` callback will be called, if
772
+ * specified, with the updated state and the `processPendingChange()` function is called again.
705
773
  */
706
- onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema<T>, id?: string) => {
774
+ processPendingChange() {
775
+ if (this.pendingChanges.length === 0) {
776
+ return;
777
+ }
778
+ const { newValue, path, id } = this.pendingChanges[0];
779
+ let { newErrorSchema } = this.pendingChanges[0];
707
780
  const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
708
- const { schemaUtils, schema } = this.state;
781
+ const { formData: oldFormData, schemaUtils, schema, errorSchema, fieldPathId } = this.state;
782
+ const rootPathId = fieldPathId.path[0] || '';
709
783
 
784
+ const isRootPath = !path || path.length === 0 || (path.length === 1 && path[0] === rootPathId);
710
785
  let retrievedSchema = this.state.retrievedSchema;
786
+ let formData = isRootPath ? newValue : _cloneDeep(oldFormData);
711
787
  if (isObject(formData) || Array.isArray(formData)) {
712
- const newState = this.getStateFromProps(this.props, formData);
788
+ if (!isRootPath) {
789
+ // If the newValue is not on the root path, then set it into the form data
790
+ _set(formData, path, newValue);
791
+ }
792
+ // Pass true to skip live validation in `getStateFromProps()` since we will do it a bit later
793
+ const newState = this.getStateFromProps(this.props, formData, undefined, undefined, undefined, true);
713
794
  formData = newState.formData;
714
795
  retrievedSchema = newState.retrievedSchema;
715
796
  }
@@ -725,7 +806,15 @@ export default class Form<
725
806
  };
726
807
  }
727
808
 
728
- if (mustValidate) {
809
+ // First update the value in the newErrorSchema in a copy of the old error schema if it was specified and the path
810
+ // is not the root
811
+ if (newErrorSchema && !isRootPath) {
812
+ const errorSchemaCopy = _cloneDeep(errorSchema);
813
+ _set(errorSchemaCopy, path, newErrorSchema);
814
+ newErrorSchema = errorSchemaCopy;
815
+ }
816
+ // If there are pending changes in the queue, skip live validation since it will happen with the last change
817
+ if (mustValidate && this.pendingChanges.length === 1) {
729
818
  const schemaValidation = this.validate(newFormData, schema, schemaUtils, retrievedSchema);
730
819
  let errors = schemaValidation.errors;
731
820
  let errorSchema = schemaValidation.errorSchema;
@@ -749,6 +838,7 @@ export default class Form<
749
838
  schemaValidationErrorSchema,
750
839
  };
751
840
  } else if (!noValidate && newErrorSchema) {
841
+ // Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
752
842
  const errorSchema = extraErrors
753
843
  ? (mergeObjects(newErrorSchema, extraErrors, 'preventDuplicates') as ErrorSchema<T>)
754
844
  : newErrorSchema;
@@ -758,8 +848,15 @@ export default class Form<
758
848
  errors: toErrorList(errorSchema),
759
849
  };
760
850
  }
761
- this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id));
762
- };
851
+ this.setState(state as FormState<T, S, F>, () => {
852
+ if (onChange) {
853
+ onChange({ ...this.state, ...state }, id);
854
+ }
855
+ // Now remove the change we just completed and call this again
856
+ this.pendingChanges.shift();
857
+ this.processPendingChange();
858
+ });
859
+ }
763
860
 
764
861
  /**
765
862
  * If the retrievedSchema has changed the new retrievedSchema is returned.
@@ -866,10 +963,28 @@ export default class Form<
866
963
  }
867
964
  };
868
965
 
966
+ /** Extracts the `GlobalFormOptions` from the given Form `props`
967
+ *
968
+ * @param props - The form props to extract the global form options from
969
+ * @returns - The `GlobalFormOptions` from the props
970
+ * @private
971
+ */
972
+ private getGlobalFormOptions(props: FormProps<T, S, F>): GlobalFormOptions {
973
+ const {
974
+ uiSchema = {},
975
+ experimental_componentUpdateStrategy,
976
+ idSeparator = DEFAULT_ID_SEPARATOR,
977
+ idPrefix = DEFAULT_ID_PREFIX,
978
+ } = props;
979
+ const rootFieldId = uiSchema['ui:rootFieldId'];
980
+ // Omit any options that are undefined or null
981
+ return { idPrefix: rootFieldId || idPrefix, idSeparator, experimental_componentUpdateStrategy };
982
+ }
983
+
869
984
  /** Returns the registry for the form */
870
985
  getRegistry(): Registry<T, S, F> {
871
986
  const { translateString: customTranslateString, uiSchema = {} } = this.props;
872
- const { schemaUtils } = this.state;
987
+ const { schema, schemaUtils } = this.state;
873
988
  const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry<T, S, F>();
874
989
  return {
875
990
  fields: { ...fields, ...this.props.fields },
@@ -882,11 +997,12 @@ export default class Form<
882
997
  },
883
998
  },
884
999
  widgets: { ...widgets, ...this.props.widgets },
885
- rootSchema: this.props.schema,
1000
+ rootSchema: schema,
886
1001
  formContext: this.props.formContext || formContext,
887
1002
  schemaUtils,
888
1003
  translateString: customTranslateString || translateString,
889
1004
  globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
1005
+ globalFormOptions: this.getGlobalFormOptions(this.props),
890
1006
  };
891
1007
  }
892
1008
 
@@ -1011,8 +1127,6 @@ export default class Form<
1011
1127
  const {
1012
1128
  children,
1013
1129
  id,
1014
- idPrefix,
1015
- idSeparator,
1016
1130
  className = '',
1017
1131
  tagName,
1018
1132
  name,
@@ -1025,12 +1139,11 @@ export default class Form<
1025
1139
  noHtml5Validate = false,
1026
1140
  disabled,
1027
1141
  readonly,
1028
- formContext,
1029
1142
  showErrorList = 'top',
1030
1143
  _internalFormWrapper,
1031
1144
  } = this.props;
1032
1145
 
1033
- const { schema, uiSchema, formData, errorSchema, idSchema } = this.state;
1146
+ const { schema, uiSchema, formData, errorSchema, fieldPathId } = this.state;
1034
1147
  const registry = this.getRegistry();
1035
1148
  const { SchemaField: _SchemaField } = registry.fields;
1036
1149
  const { SubmitButton } = registry.templates.ButtonTemplates;
@@ -1068,10 +1181,7 @@ export default class Form<
1068
1181
  schema={schema}
1069
1182
  uiSchema={uiSchema}
1070
1183
  errorSchema={errorSchema}
1071
- idSchema={idSchema}
1072
- idPrefix={idPrefix}
1073
- idSeparator={idSeparator}
1074
- formContext={formContext}
1184
+ fieldPathId={fieldPathId}
1075
1185
  formData={formData}
1076
1186
  onChange={this.onChange}
1077
1187
  onBlur={this.onBlur}