@rjsf/core 6.0.0-beta.13 → 6.0.0-beta.14

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 (46) hide show
  1. package/dist/core.umd.js +235 -121
  2. package/dist/index.esm.js +277 -157
  3. package/dist/index.esm.js.map +3 -3
  4. package/dist/index.js +301 -182
  5. package/dist/index.js.map +3 -3
  6. package/lib/components/Form.d.ts +43 -12
  7. package/lib/components/Form.d.ts.map +1 -1
  8. package/lib/components/Form.js +70 -22
  9. package/lib/components/fields/ArrayField.d.ts +14 -4
  10. package/lib/components/fields/ArrayField.d.ts.map +1 -1
  11. package/lib/components/fields/ArrayField.js +74 -28
  12. package/lib/components/fields/BooleanField.d.ts.map +1 -1
  13. package/lib/components/fields/BooleanField.js +6 -1
  14. package/lib/components/fields/LayoutGridField.d.ts +19 -1
  15. package/lib/components/fields/LayoutGridField.d.ts.map +1 -1
  16. package/lib/components/fields/LayoutGridField.js +62 -12
  17. package/lib/components/fields/LayoutMultiSchemaField.d.ts.map +1 -1
  18. package/lib/components/fields/LayoutMultiSchemaField.js +2 -1
  19. package/lib/components/fields/MultiSchemaField.d.ts.map +1 -1
  20. package/lib/components/fields/MultiSchemaField.js +2 -1
  21. package/lib/components/fields/NullField.js +3 -3
  22. package/lib/components/fields/NumberField.js +2 -2
  23. package/lib/components/fields/ObjectField.d.ts +2 -2
  24. package/lib/components/fields/ObjectField.d.ts.map +1 -1
  25. package/lib/components/fields/ObjectField.js +16 -19
  26. package/lib/components/fields/SchemaField.js +2 -2
  27. package/lib/components/fields/StringField.d.ts.map +1 -1
  28. package/lib/components/fields/StringField.js +6 -1
  29. package/lib/components/widgets/AltDateWidget.d.ts.map +1 -1
  30. package/lib/components/widgets/AltDateWidget.js +15 -18
  31. package/lib/components/widgets/CheckboxesWidget.js +2 -2
  32. package/lib/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +11 -11
  34. package/src/components/Form.tsx +85 -22
  35. package/src/components/fields/ArrayField.tsx +75 -29
  36. package/src/components/fields/BooleanField.tsx +10 -1
  37. package/src/components/fields/LayoutGridField.tsx +69 -11
  38. package/src/components/fields/LayoutMultiSchemaField.tsx +2 -1
  39. package/src/components/fields/MultiSchemaField.tsx +2 -1
  40. package/src/components/fields/NullField.tsx +3 -3
  41. package/src/components/fields/NumberField.tsx +2 -2
  42. package/src/components/fields/ObjectField.tsx +16 -26
  43. package/src/components/fields/SchemaField.tsx +2 -2
  44. package/src/components/fields/StringField.tsx +10 -1
  45. package/src/components/widgets/AltDateWidget.tsx +20 -22
  46. package/src/components/widgets/CheckboxesWidget.tsx +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rjsf/core",
3
- "version": "6.0.0-beta.13",
3
+ "version": "6.0.0-beta.14",
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",
@@ -66,30 +66,30 @@
66
66
  "node": ">=20"
67
67
  },
68
68
  "peerDependencies": {
69
- "@rjsf/utils": "^6.0.0-beta",
69
+ "@rjsf/utils": "^6.0.0-beta.14",
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",
75
+ "markdown-to-jsx": "^7.7.13",
76
76
  "nanoid": "^5.1.5",
77
77
  "prop-types": "^15.8.1"
78
78
  },
79
79
  "devDependencies": {
80
- "@rjsf/snapshot-tests": "^6.0.0-beta.13",
81
- "@rjsf/utils": "^6.0.0-beta.13",
82
- "@rjsf/validator-ajv8": "^6.0.0-beta.13",
83
- "@testing-library/jest-dom": "^6.6.3",
84
- "@testing-library/react": "^16.2.0",
80
+ "@rjsf/snapshot-tests": "^6.0.0-beta.14",
81
+ "@rjsf/utils": "^6.0.0-beta.14",
82
+ "@rjsf/validator-ajv8": "^6.0.0-beta.14",
83
+ "@testing-library/jest-dom": "^6.6.4",
84
+ "@testing-library/react": "^16.3.0",
85
85
  "@testing-library/user-event": "^14.6.1",
86
- "ajv": "^8.12.0",
86
+ "ajv": "^8.17.1",
87
87
  "atob": "^2.1.2",
88
88
  "chai": "^3.5.0",
89
- "eslint": "^8.56.0",
89
+ "eslint": "^8.57.1",
90
90
  "html": "^1.0.0",
91
91
  "jsdom": "^20.0.3",
92
- "mocha": "^10.2.0",
92
+ "mocha": "^10.8.2",
93
93
  "react-portal": "^4.3.0",
94
94
  "sinon": "^9.2.4"
95
95
  },
@@ -38,11 +38,13 @@ import {
38
38
  createErrorHandler,
39
39
  unwrapErrorHandler,
40
40
  } from '@rjsf/utils';
41
+ import _cloneDeep from 'lodash/cloneDeep';
41
42
  import _forEach from 'lodash/forEach';
42
43
  import _get from 'lodash/get';
43
44
  import _isEmpty from 'lodash/isEmpty';
44
45
  import _isNil from 'lodash/isNil';
45
46
  import _pick from 'lodash/pick';
47
+ import _set from 'lodash/set';
46
48
  import _toPath from 'lodash/toPath';
47
49
 
48
50
  import getDefaultRegistry from '../getDefaultRegistry';
@@ -272,6 +274,19 @@ export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema,
272
274
  status?: 'submitted';
273
275
  }
274
276
 
277
+ /** The definition of a pending change that will be processed in the `onChange` handler
278
+ */
279
+ interface PendingChange<T> {
280
+ /** The path into the formData/errorSchema at which the `newValue`/`newErrorSchema` will be set */
281
+ path?: (number | string)[];
282
+ /** The new value to set into the formData */
283
+ newValue?: T;
284
+ /** The new errors to be set into the errorSchema, if any */
285
+ newErrorSchema?: ErrorSchema<T>;
286
+ /** The optional id of the field for which the change is being made */
287
+ id?: string;
288
+ }
289
+
275
290
  /** The `Form` component renders the outer form and all the fields defined in the `schema` */
276
291
  export default class Form<
277
292
  T = any,
@@ -283,6 +298,10 @@ export default class Form<
283
298
  */
284
299
  formElement: RefObject<any>;
285
300
 
301
+ /** The list of pending changes
302
+ */
303
+ pendingChanges: PendingChange<T>[] = [];
304
+
286
305
  /** Constructs the `Form` from the `props`. Will setup the initial state from the props. It will also call the
287
306
  * `onChange` handler if the initially provided `formData` is modified to add missing default values as part of the
288
307
  * state construction.
@@ -539,8 +558,7 @@ export default class Form<
539
558
  let customValidateErrors = {};
540
559
  if (typeof customValidate === 'function') {
541
560
  const errorHandler = customValidate(prevFormData, createErrorHandler<T>(prevFormData), uiSchema);
542
- const userErrorSchema = unwrapErrorHandler<T>(errorHandler);
543
- customValidateErrors = userErrorSchema;
561
+ customValidateErrors = unwrapErrorHandler<T>(errorHandler);
544
562
  }
545
563
  return customValidateErrors;
546
564
  }
@@ -550,7 +568,8 @@ export default class Form<
550
568
  *
551
569
  * @param formData - The new form data to validate
552
570
  * @param schema - The schema used to validate against
553
- * @param altSchemaUtils - The alternate schemaUtils to use for validation
571
+ * @param [altSchemaUtils] - The alternate schemaUtils to use for validation
572
+ * @param [retrievedSchema] - An optionally retrieved schema for per
554
573
  */
555
574
  validate(
556
575
  formData: T | undefined,
@@ -655,11 +674,16 @@ export default class Form<
655
674
  const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
656
675
  const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData);
657
676
  const fieldNames = this.getFieldNames(pathSchema, formData);
658
- const newFormData = this.getUsedFormData(formData, fieldNames);
659
- return newFormData;
677
+ return this.getUsedFormData(formData, fieldNames);
660
678
  };
661
679
 
662
- // Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
680
+ /** Filtering errors based on your retrieved schema to only show errors for properties in the selected branch.
681
+ *
682
+ * @param schemaErrors - The schema errors to filter
683
+ * @param [resolvedSchema] - An optionally resolved schema to use for performance reasons
684
+ * @param [formData] - The formData to help filter errors
685
+ * @private
686
+ */
663
687
  private filterErrorsBasedOnSchema(schemaErrors: ErrorSchema<T>, resolvedSchema?: S, formData?: any): ErrorSchema<T> {
664
688
  const { retrievedSchema, schemaUtils } = this.state;
665
689
  const _retrievedSchema = resolvedSchema ?? retrievedSchema;
@@ -705,23 +729,47 @@ export default class Form<
705
729
  return filterNilOrEmptyErrors(filteredErrors, prevCustomValidateErrors);
706
730
  }
707
731
 
708
- /** Function to handle changes made to a field in the `Form`. This handler receives an entirely new copy of the
709
- * `formData` along with a new `ErrorSchema`. It will first update the `formData` with any missing default fields and
710
- * then, if `omitExtraData` and `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not
711
- * in a form field. Then, the resulting formData will be validated if required. The state will be updated with the new
712
- * updated (potentially filtered) `formData`, any errors that resulted from validation. Finally the `onChange`
713
- * callback will be called if specified with the updated state.
732
+ /** Pushes the given change information into the `pendingChanges` array and then calls `processPendingChanges()` if
733
+ * the array only contains a single pending change.
714
734
  *
715
- * @param formData - The new form data from a change to a field
716
- * @param newErrorSchema - The new `ErrorSchema` based on the field change
717
- * @param id - The id of the field that caused the change
735
+ * @param newValue - The new form data from a change to a field
736
+ * @param [path] - The path to the change into which to set the formData
737
+ * @param [newErrorSchema] - The new `ErrorSchema` based on the field change
738
+ * @param [id] - The id of the field that caused the change
739
+ */
740
+ onChange = (newValue: T | undefined, path?: (number | string)[], newErrorSchema?: ErrorSchema<T>, id?: string) => {
741
+ this.pendingChanges.push({ newValue, path, newErrorSchema, id });
742
+ if (this.pendingChanges.length === 1) {
743
+ this.processPendingChange();
744
+ }
745
+ };
746
+
747
+ /** Function to handle changes made to a field in the `Form`. This handler gets the first change from the
748
+ * `pendingChanges` list, containing the `newValue` for the `formData` and the `path` at which the `newValue` is to be
749
+ * updated, along with a new, optional `ErrorSchema` for that same `path` and potentially the `id` of the field being
750
+ * changed. It will first update the `formData` with any missing default fields and then, if `omitExtraData` and
751
+ * `liveOmit` are turned on, the `formData` will be filtered to remove any extra data not in a form field. Then, the
752
+ * resulting `formData` will be validated if required. The state will be updated with the new updated (potentially
753
+ * filtered) `formData`, any errors that resulted from validation. Finally the `onChange` callback will be called, if
754
+ * specified, with the updated state and the `processPendingChange()` function is called again.
718
755
  */
719
- onChange = (formData: T | undefined, newErrorSchema?: ErrorSchema<T>, id?: string) => {
720
- const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
721
- const { schemaUtils, schema } = this.state;
756
+ processPendingChange() {
757
+ if (this.pendingChanges.length === 0) {
758
+ return;
759
+ }
760
+ const { newValue, path, id } = this.pendingChanges[0];
761
+ let { newErrorSchema } = this.pendingChanges[0];
762
+ const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange, idPrefix = '' } = this.props;
763
+ const { formData: oldFormData, schemaUtils, schema, errorSchema } = this.state;
722
764
 
765
+ const isRootPath = !path || path.length === 0 || (path.length === 1 && path[0] === idPrefix);
723
766
  let retrievedSchema = this.state.retrievedSchema;
767
+ let formData = isRootPath ? newValue : _cloneDeep(oldFormData);
724
768
  if (isObject(formData) || Array.isArray(formData)) {
769
+ if (!isRootPath) {
770
+ // If the newValue is not on the root path, then set it into the form data
771
+ _set(formData, path, newValue);
772
+ }
725
773
  const newState = this.getStateFromProps(this.props, formData);
726
774
  formData = newState.formData;
727
775
  retrievedSchema = newState.retrievedSchema;
@@ -738,6 +786,13 @@ export default class Form<
738
786
  };
739
787
  }
740
788
 
789
+ // First update the value in the newErrorSchema in a copy of the old error schema if it was specified and the path
790
+ // is not the root
791
+ if (newErrorSchema && !isRootPath) {
792
+ const errorSchemaCopy = _cloneDeep(errorSchema);
793
+ _set(errorSchemaCopy, path, newErrorSchema);
794
+ newErrorSchema = errorSchemaCopy;
795
+ }
741
796
  if (mustValidate) {
742
797
  const schemaValidation = this.validate(newFormData, schema, schemaUtils, retrievedSchema);
743
798
  let errors = schemaValidation.errors;
@@ -762,6 +817,7 @@ export default class Form<
762
817
  schemaValidationErrorSchema,
763
818
  };
764
819
  } else if (!noValidate && newErrorSchema) {
820
+ // Merging 'newErrorSchema' into 'errorSchema' to display the custom raised errors.
765
821
  const errorSchema = extraErrors
766
822
  ? (mergeObjects(newErrorSchema, extraErrors, 'preventDuplicates') as ErrorSchema<T>)
767
823
  : newErrorSchema;
@@ -771,8 +827,15 @@ export default class Form<
771
827
  errors: toErrorList(errorSchema),
772
828
  };
773
829
  }
774
- this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id));
775
- };
830
+ this.setState(state as FormState<T, S, F>, () => {
831
+ if (onChange) {
832
+ onChange({ ...this.state, ...state }, id);
833
+ }
834
+ // Now remove the change we just completed and call this again
835
+ this.pendingChanges.shift();
836
+ this.processPendingChange();
837
+ });
838
+ }
776
839
 
777
840
  /**
778
841
  * If the retrievedSchema has changed the new retrievedSchema is returned.
@@ -1029,7 +1092,7 @@ export default class Form<
1029
1092
  const {
1030
1093
  children,
1031
1094
  id,
1032
- idPrefix,
1095
+ idPrefix = '',
1033
1096
  idSeparator,
1034
1097
  className = '',
1035
1098
  tagName,
@@ -1082,7 +1145,7 @@ export default class Form<
1082
1145
  >
1083
1146
  {showErrorList === 'top' && this.renderErrors(registry)}
1084
1147
  <_SchemaField
1085
- name=''
1148
+ name={idPrefix}
1086
1149
  schema={schema}
1087
1150
  uiSchema={uiSchema}
1088
1151
  errorSchema={errorSchema}
@@ -22,7 +22,7 @@ import cloneDeep from 'lodash/cloneDeep';
22
22
  import get from 'lodash/get';
23
23
  import isObject from 'lodash/isObject';
24
24
  import set from 'lodash/set';
25
- import { nanoid } from 'nanoid';
25
+ import uniqueId from 'lodash/uniqueId';
26
26
 
27
27
  /** Type used to represent the keyed form data used in the state */
28
28
  type KeyedFormDataType<T> = { key: string; item: T };
@@ -37,7 +37,7 @@ type ArrayFieldState<T> = {
37
37
 
38
38
  /** Used to generate a unique ID for an element in a row */
39
39
  function generateRowId() {
40
- return nanoid();
40
+ return uniqueId('rjsf-array-item-');
41
41
  }
42
42
 
43
43
  /** Converts the `formData` into `KeyedFormDataType` data, using the `generateRowId()` function to create the key
@@ -229,7 +229,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
229
229
  keyedFormData: newKeyedFormData,
230
230
  updatedKeyedFormData: true,
231
231
  },
232
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
232
+ // add click will pass the empty `path` array to the onChange which adds the appropriate path
233
+ () => onChange(keyedToPlainFormData(newKeyedFormData), [], newErrorSchema as ErrorSchema<T[]>),
233
234
  );
234
235
  }
235
236
 
@@ -298,7 +299,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
298
299
  keyedFormData: newKeyedFormData,
299
300
  updatedKeyedFormData: true,
300
301
  },
301
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
302
+ // Copy index will pass the empty `path` array to the onChange which adds the appropriate path
303
+ () => onChange(keyedToPlainFormData(newKeyedFormData), [], newErrorSchema as ErrorSchema<T[]>),
302
304
  );
303
305
  };
304
306
  };
@@ -335,7 +337,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
335
337
  keyedFormData: newKeyedFormData,
336
338
  updatedKeyedFormData: true,
337
339
  },
338
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
340
+ // drop index will pass the empty `path` array to the onChange which adds the appropriate path
341
+ () => onChange(keyedToPlainFormData(newKeyedFormData), [], newErrorSchema as ErrorSchema<T[]>),
339
342
  );
340
343
  };
341
344
  };
@@ -385,7 +388,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
385
388
  {
386
389
  keyedFormData: newKeyedFormData,
387
390
  },
388
- () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema as ErrorSchema<T[]>),
391
+ // reorder click will pass the empty `path` array to the onChange which adds the appropriate path
392
+ () => onChange(keyedToPlainFormData(newKeyedFormData), [], newErrorSchema as ErrorSchema<T[]>),
389
393
  );
390
394
  };
391
395
  };
@@ -396,22 +400,17 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
396
400
  * @param index - The index of the item being changed
397
401
  */
398
402
  onChangeForIndex = (index: number) => {
399
- return (value: any, newErrorSchema?: ErrorSchema<T>, id?: string) => {
400
- const { formData, onChange, errorSchema } = this.props;
401
- const arrayData = Array.isArray(formData) ? formData : [];
402
- const newFormData = arrayData.map((item: T, i: number) => {
403
+ return (value: any, path?: (number | string)[], newErrorSchema?: ErrorSchema<T>, id?: string) => {
404
+ const { onChange } = this.props;
405
+ // Copy the current path and insert in the index into the first location
406
+ const changePath = Array.isArray(path) ? path.slice() : [];
407
+ changePath.unshift(index);
408
+ onChange(
403
409
  // We need to treat undefined items as nulls to have validation.
404
410
  // See https://github.com/tdegrunt/jsonschema/issues/206
405
- const jsonValue = typeof value === 'undefined' ? null : value;
406
- return index === i ? jsonValue : item;
407
- });
408
- onChange(
409
- newFormData,
410
- errorSchema &&
411
- errorSchema && {
412
- ...errorSchema,
413
- [index]: newErrorSchema,
414
- },
411
+ value === undefined ? null : value,
412
+ changePath,
413
+ newErrorSchema as ErrorSchema<T[]>,
415
414
  id,
416
415
  );
417
416
  };
@@ -419,10 +418,44 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
419
418
 
420
419
  /** Callback handler used to change the value for a checkbox */
421
420
  onSelectChange = (value: any) => {
422
- const { onChange, idSchema } = this.props;
423
- onChange(value, undefined, idSchema && idSchema.$id);
421
+ const { name, onChange, idSchema } = this.props;
422
+ // select change will pass the `path` array with the name
423
+ onChange(value, [name], undefined, idSchema && idSchema.$id);
424
424
  };
425
425
 
426
+ /** Helper method to compute item UI schema for both normal and fixed arrays
427
+ * Handles both static object and dynamic function cases
428
+ *
429
+ * @param uiSchema - The parent UI schema containing items definition
430
+ * @param item - The item data
431
+ * @param index - The index of the item
432
+ * @param formContext - The form context
433
+ * @returns The computed UI schema for the item
434
+ */
435
+ private computeItemUiSchema(
436
+ uiSchema: UiSchema<T[], S, F>,
437
+ item: T,
438
+ index: number,
439
+ formContext: F,
440
+ ): UiSchema<T[], S, F> | undefined {
441
+ if (typeof uiSchema.items === 'function') {
442
+ try {
443
+ // Call the function with item data, index, and form context
444
+ // TypeScript now correctly infers the types thanks to the ArrayElement type in UiSchema
445
+ const result = uiSchema.items(item, index, formContext);
446
+ // Only use the result if it's truthy
447
+ return result as UiSchema<T[], S, F>;
448
+ } catch (e) {
449
+ console.error(`Error executing dynamic uiSchema.items function for item at index ${index}:`, e);
450
+ // Fall back to undefined to allow the field to still render
451
+ return undefined;
452
+ }
453
+ } else {
454
+ // Static object case - preserve undefined to maintain backward compatibility
455
+ return uiSchema.items as UiSchema<T[], S, F> | undefined;
456
+ }
457
+ }
458
+
426
459
  /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements
427
460
  */
428
461
  render() {
@@ -500,6 +533,10 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
500
533
  const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
501
534
  const itemIdPrefix = idSchema.$id + idSeparator + index;
502
535
  const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
536
+
537
+ // Compute the item UI schema using the helper method
538
+ const itemUiSchema = this.computeItemUiSchema(uiSchema, item, index, formContext);
539
+
503
540
  return this.renderArrayFieldItem({
504
541
  key,
505
542
  index,
@@ -512,7 +549,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
512
549
  itemIdSchema,
513
550
  itemErrorSchema,
514
551
  itemData: itemCast,
515
- itemUiSchema: uiSchema.items,
552
+ itemUiSchema,
516
553
  autofocus: autofocus && index === 0,
517
554
  onBlur,
518
555
  onFocus,
@@ -751,11 +788,20 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
751
788
  : itemSchemas[index]) || {};
752
789
  const itemIdPrefix = idSchema.$id + idSeparator + index;
753
790
  const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
754
- const itemUiSchema = additional
755
- ? uiSchema.additionalItems || {}
756
- : Array.isArray(uiSchema.items)
757
- ? uiSchema.items[index]
758
- : uiSchema.items || {};
791
+ // Compute the item UI schema - handle both static and dynamic cases
792
+ let itemUiSchema: UiSchema<T[], S, F> | undefined;
793
+ if (additional) {
794
+ // For additional items, use additionalItems uiSchema
795
+ itemUiSchema = uiSchema.additionalItems as UiSchema<T[], S, F>;
796
+ } else {
797
+ // For fixed items, uiSchema.items can be an array, a function, or a single object
798
+ if (Array.isArray(uiSchema.items)) {
799
+ itemUiSchema = uiSchema.items[index] as UiSchema<T[], S, F>;
800
+ } else {
801
+ // Use the helper method for function or static object cases
802
+ itemUiSchema = this.computeItemUiSchema(uiSchema, item, index, formContext);
803
+ }
804
+ }
759
805
  const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
760
806
 
761
807
  return this.renderArrayFieldItem({
@@ -811,7 +857,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
811
857
  canMoveDown: boolean;
812
858
  itemSchema: S;
813
859
  itemData: T[];
814
- itemUiSchema: UiSchema<T[], S, F>;
860
+ itemUiSchema: UiSchema<T[], S, F> | undefined;
815
861
  itemIdSchema: IdSchema<T[]>;
816
862
  itemErrorSchema?: ErrorSchema<T[]>;
817
863
  autofocus?: boolean;
@@ -1,3 +1,4 @@
1
+ import { useCallback } from 'react';
1
2
  import {
2
3
  getWidget,
3
4
  getUiOptions,
@@ -5,6 +6,7 @@ import {
5
6
  FieldProps,
6
7
  FormContextType,
7
8
  EnumOptionsType,
9
+ ErrorSchema,
8
10
  RJSFSchema,
9
11
  StrictRJSFSchema,
10
12
  TranslatableString,
@@ -86,6 +88,13 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
86
88
  enumOptions = optionsList<T, S, F>({ enum: enums } as S, uiSchema);
87
89
  }
88
90
  }
91
+ const onWidgetChange = useCallback(
92
+ (value: T | undefined, errorSchema?: ErrorSchema, id?: string) => {
93
+ // Boolean field change passes an empty path array to the parent field which adds the appropriate path
94
+ return onChange(value, [], errorSchema, id);
95
+ },
96
+ [onChange],
97
+ );
89
98
 
90
99
  return (
91
100
  <Widget
@@ -94,7 +103,7 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
94
103
  uiSchema={uiSchema}
95
104
  id={idSchema.$id}
96
105
  name={name}
97
- onChange={onChange}
106
+ onChange={onWidgetChange}
98
107
  onFocus={onFocus}
99
108
  onBlur={onBlur}
100
109
  label={label}
@@ -24,6 +24,7 @@ import {
24
24
  UI_OPTIONS_KEY,
25
25
  UI_GLOBAL_OPTIONS_KEY,
26
26
  UiSchema,
27
+ ITEMS_KEY,
27
28
  } from '@rjsf/utils';
28
29
  import cloneDeep from 'lodash/cloneDeep';
29
30
  import each from 'lodash/each';
@@ -39,6 +40,7 @@ import isObject from 'lodash/isObject';
39
40
  import isPlainObject from 'lodash/isPlainObject';
40
41
  import isString from 'lodash/isString';
41
42
  import isUndefined from 'lodash/isUndefined';
43
+ import last from 'lodash/last';
42
44
  import set from 'lodash/set';
43
45
 
44
46
  /** The enumeration of the three different Layout GridTemplate type values
@@ -130,6 +132,15 @@ function getNonNullishValue<T = unknown>(value?: T, fallback?: T): T | undefined
130
132
  return value ?? fallback;
131
133
  }
132
134
 
135
+ /** Detects if a `str` is made up entirely of numeric characters
136
+ *
137
+ * @param str - The string to check to see if it is a numeric index
138
+ * @return - True if the string consists entirely of numeric characters
139
+ */
140
+ function isNumericIndex(str: string) {
141
+ return /^\d+?$/.test(str); // Matches positive integers
142
+ }
143
+
133
144
  /** The `LayoutGridField` will render a schema, uiSchema and formData combination out into a GridTemplate in the shape
134
145
  * described in the uiSchema. To define the grid to use to render the elements within a field in the schema, provide in
135
146
  * the uiSchema for that field the object contained under a `ui:layoutGrid` element. E.g. (as a JSON object):
@@ -496,6 +507,47 @@ export default class LayoutGridField<
496
507
  return schemaUtils.toIdSchema(schema, baseId, formData, baseId, idSeparator);
497
508
  }
498
509
 
510
+ /** Computes the `rawSchema` and `idSchema` for a `schema` and a `potentialIndex`. If the `schema` is of type array,
511
+ * has an `ITEMS_KEY` element and `potentialIndex` represents a numeric value, the element at `ITEMS_KEY` is checked
512
+ * to see if it is an array. If it is AND the `potentialIndex`th element is available, it is used as the `rawSchema`,
513
+ * otherwise the last value of the element is used. If it is not, then the element is used as the `rawSchema`. In
514
+ * either case, an `idSchema` is computed for the array index. If the `schema` does not represent an array or the
515
+ * `potentialIndex` is not a numeric value, then `rawSchema` is returned as undefined and given `idSchema` is returned
516
+ * as is.
517
+ *
518
+ * @param schema - The schema to generate the idSchema for
519
+ * @param idSchema - The IdSchema for the schema
520
+ * @param potentialIndex - A string containing a potential index
521
+ * @param [idSeparator] - The param to pass into the `toIdSchema` util which will use it to join the `idSchema` paths
522
+ * @returns - An object containing the `rawSchema` and `idSchema` of an array item, otherwise an undefined `rawSchema`
523
+ */
524
+ static computeArraySchemasIfPresent<T = any, S extends StrictRJSFSchema = RJSFSchema>(
525
+ schema: S | undefined,
526
+ idSchema: IdSchema<T>,
527
+ potentialIndex: string,
528
+ idSeparator?: string,
529
+ ): {
530
+ rawSchema?: S;
531
+ idSchema: IdSchema<T>;
532
+ } {
533
+ let rawSchema: S | undefined;
534
+ if (isNumericIndex(potentialIndex) && schema && schema?.type === 'array' && has(schema, ITEMS_KEY)) {
535
+ const index = Number(potentialIndex);
536
+ const items = schema[ITEMS_KEY];
537
+ if (Array.isArray(items)) {
538
+ if (index > items.length) {
539
+ rawSchema = last(items) as S;
540
+ } else {
541
+ rawSchema = items[index] as S;
542
+ }
543
+ } else {
544
+ rawSchema = items as S;
545
+ }
546
+ idSchema = { [ID_KEY]: `${idSchema[ID_KEY]}${idSeparator ?? '_'}${index}` } as IdSchema<T>;
547
+ }
548
+ return { rawSchema, idSchema };
549
+ }
550
+
499
551
  /** Given a `dottedPath` to a field in the `initialSchema`, iterate through each individual path in the schema until
500
552
  * the leaf path is found and returned (along with whether that leaf path `isRequired`) OR no schema exists for an
501
553
  * element in the path. If the leaf schema element happens to be a oneOf/anyOf then also return the oneOf/anyOf as
@@ -552,7 +604,9 @@ export default class LayoutGridField<
552
604
  rawSchema = get(selectedSchema, [PROPERTIES_KEY, part], {}) as S;
553
605
  idSchema = get(selectedIdSchema, part, {}) as IdSchema<T>;
554
606
  } else {
555
- rawSchema = {} as S;
607
+ const result = LayoutGridField.computeArraySchemasIfPresent<T, S>(schema, idSchema, part, idSeparator);
608
+ rawSchema = result.rawSchema ?? ({} as S);
609
+ idSchema = result.idSchema;
556
610
  }
557
611
  // Now drill into the innerData for the part, returning an empty object by default if it doesn't exist
558
612
  innerData = get(innerData, part, {}) as T;
@@ -578,11 +632,17 @@ export default class LayoutGridField<
578
632
  idSchema = mergeObjects(rawIdSchema, idSchema) as IdSchema<T>;
579
633
  }
580
634
  isRequired = schema !== undefined && Array.isArray(schema.required) && includes(schema.required, leafPath);
581
- // Now grab the schema from the leafPath of the current schema properties
582
- schema = get(schema, [PROPERTIES_KEY, leafPath]) as S | undefined;
583
- // Resolve any `$ref`s for the current schema
584
- schema = schema ? schemaUtils.retrieveSchema(schema) : schema;
585
- idSchema = get(idSchema, leafPath, {}) as IdSchema<T>;
635
+ const result = LayoutGridField.computeArraySchemasIfPresent<T, S>(schema, idSchema, leafPath, idSeparator);
636
+ if (result.rawSchema) {
637
+ schema = result.rawSchema;
638
+ idSchema = result.idSchema;
639
+ } else {
640
+ // Now grab the schema from the leafPath of the current schema properties
641
+ schema = get(schema, [PROPERTIES_KEY, leafPath]) as S | undefined;
642
+ // Resolve any `$ref`s for the current schema
643
+ schema = schema ? schemaUtils.retrieveSchema(schema) : schema;
644
+ idSchema = get(idSchema, leafPath, {}) as IdSchema<T>;
645
+ }
586
646
  isReadonly = getNonNullishValue(schema?.readOnly, isReadonly);
587
647
  if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) {
588
648
  const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY;
@@ -676,16 +736,14 @@ export default class LayoutGridField<
676
736
  * @returns - The `onChange` handling function for the `dottedPath` field
677
737
  */
678
738
  onFieldChange = (dottedPath: string) => {
679
- return (value: unknown, errSchema?: ErrorSchema<T>, id?: string) => {
680
- const { onChange, errorSchema, formData } = this.props;
681
- const newFormData = cloneDeep(formData || ({} as T));
739
+ return (value: T | undefined, path?: (number | string)[], errSchema?: ErrorSchema<T>, id?: string) => {
740
+ const { onChange, errorSchema } = this.props;
682
741
  let newErrorSchema = errorSchema;
683
742
  if (errSchema && errorSchema) {
684
743
  newErrorSchema = cloneDeep(errorSchema);
685
744
  set(newErrorSchema, dottedPath, errSchema);
686
745
  }
687
- set(newFormData as object, dottedPath, value);
688
- onChange(newFormData, newErrorSchema, id);
746
+ onChange(value, dottedPath.split('.'), newErrorSchema, id);
689
747
  };
690
748
  };
691
749
 
@@ -171,7 +171,8 @@ export default function LayoutMultiSchemaField<
171
171
  if (newFormData) {
172
172
  set(newFormData, selectorField, opt);
173
173
  }
174
- onChange(newFormData, undefined, id);
174
+ // Pass the component name in the path
175
+ onChange(newFormData, [name], undefined, id);
175
176
  };
176
177
 
177
178
  // filtering the options based on the type of widget because `selectField` does not recognize the `convertOther` prop
@@ -131,7 +131,8 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
131
131
  }
132
132
 
133
133
  this.setState({ selectedOption: intOption }, () => {
134
- onChange(newFormData, undefined, this.getFieldId());
134
+ // Changing the option will pass an empty path array to the parent field which will add the appropriate path
135
+ onChange(newFormData, [], undefined, this.getFieldId());
135
136
  });
136
137
  };
137
138
 
@@ -9,12 +9,12 @@ import { FieldProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf
9
9
  function NullField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
10
10
  props: FieldProps<T, S, F>,
11
11
  ) {
12
- const { formData, onChange } = props;
12
+ const { name, formData, onChange } = props;
13
13
  useEffect(() => {
14
14
  if (formData === undefined) {
15
- onChange(null as unknown as T);
15
+ onChange(null as unknown as T, [name]);
16
16
  }
17
- }, [formData, onChange]);
17
+ }, [name, formData, onChange]);
18
18
 
19
19
  return null;
20
20
  }
@@ -44,7 +44,7 @@ function NumberField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
44
44
  * @param value - The current value for the change occurring
45
45
  */
46
46
  const handleChange = useCallback(
47
- (value: FieldProps<T, S, F>['value'], errorSchema?: ErrorSchema<T>, id?: string) => {
47
+ (value: FieldProps<T, S, F>['value'], path?: (number | string)[], errorSchema?: ErrorSchema<T>, id?: string) => {
48
48
  // Cache the original value in component state
49
49
  setLastValue(value);
50
50
 
@@ -62,7 +62,7 @@ function NumberField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
62
62
  ? asNumber(value.replace(trailingCharMatcher, ''))
63
63
  : asNumber(value);
64
64
 
65
- onChange(processed as unknown as T, errorSchema, id);
65
+ onChange(processed as unknown as T, path, errorSchema, id);
66
66
  },
67
67
  [onChange],
68
68
  );