@rjsf/core 6.1.1 → 6.2.3

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 (35) hide show
  1. package/dist/core.umd.js +65 -44
  2. package/dist/index.cjs +65 -44
  3. package/dist/index.cjs.map +3 -3
  4. package/dist/index.esm.js +68 -47
  5. package/dist/index.esm.js.map +3 -3
  6. package/lib/components/Form.d.ts +17 -5
  7. package/lib/components/Form.d.ts.map +1 -1
  8. package/lib/components/Form.js +35 -49
  9. package/lib/components/fields/LayoutMultiSchemaField.d.ts.map +1 -1
  10. package/lib/components/fields/LayoutMultiSchemaField.js +1 -1
  11. package/lib/components/fields/SchemaField.d.ts.map +1 -1
  12. package/lib/components/fields/SchemaField.js +1 -0
  13. package/lib/components/templates/BaseInputTemplate.d.ts.map +1 -1
  14. package/lib/components/templates/BaseInputTemplate.js +7 -1
  15. package/lib/components/templates/ButtonTemplates/IconButton.d.ts +1 -0
  16. package/lib/components/templates/ButtonTemplates/IconButton.d.ts.map +1 -1
  17. package/lib/components/templates/ButtonTemplates/IconButton.js +4 -0
  18. package/lib/components/templates/ButtonTemplates/index.d.ts.map +1 -1
  19. package/lib/components/templates/ButtonTemplates/index.js +2 -1
  20. package/lib/components/templates/ObjectFieldTemplate.d.ts +1 -1
  21. package/lib/components/templates/ObjectFieldTemplate.d.ts.map +1 -1
  22. package/lib/components/templates/ObjectFieldTemplate.js +6 -0
  23. package/lib/components/widgets/TextareaWidget.d.ts +0 -6
  24. package/lib/components/widgets/TextareaWidget.d.ts.map +1 -1
  25. package/lib/components/widgets/TextareaWidget.js +0 -4
  26. package/lib/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +7 -10
  28. package/src/components/Form.tsx +53 -62
  29. package/src/components/fields/LayoutMultiSchemaField.tsx +1 -0
  30. package/src/components/fields/SchemaField.tsx +1 -0
  31. package/src/components/templates/BaseInputTemplate.tsx +13 -1
  32. package/src/components/templates/ButtonTemplates/IconButton.tsx +24 -0
  33. package/src/components/templates/ButtonTemplates/index.ts +2 -1
  34. package/src/components/templates/ObjectFieldTemplate.tsx +9 -0
  35. package/src/components/widgets/TextareaWidget.tsx +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rjsf/core",
3
- "version": "6.1.1",
3
+ "version": "6.2.3",
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,7 +66,7 @@
66
66
  "node": ">=20"
67
67
  },
68
68
  "peerDependencies": {
69
- "@rjsf/utils": "^6.x",
69
+ "@rjsf/utils": "^6.2.x",
70
70
  "react": ">=18"
71
71
  },
72
72
  "dependencies": {
@@ -76,21 +76,18 @@
76
76
  "prop-types": "^15.8.1"
77
77
  },
78
78
  "devDependencies": {
79
- "@rjsf/snapshot-tests": "^6.x",
80
- "@rjsf/utils": "^6.x",
81
- "@rjsf/validator-ajv8": "^6.x",
79
+ "@rjsf/snapshot-tests": "^6.2.0",
80
+ "@rjsf/utils": "^6.2.0",
81
+ "@rjsf/validator-ajv8": "^6.2.0",
82
82
  "@testing-library/jest-dom": "^6.9.1",
83
83
  "@testing-library/react": "^16.3.0",
84
84
  "@testing-library/user-event": "^14.6.1",
85
+ "@types/react-portal": "^4.0.7",
85
86
  "ajv": "^8.17.1",
86
87
  "atob": "^2.1.2",
87
- "chai": "^3.5.0",
88
88
  "eslint": "^8.57.1",
89
- "html": "^1.0.0",
90
89
  "jsdom": "^27.0.1",
91
- "mocha": "^11.7.4",
92
- "react-portal": "^4.3.0",
93
- "sinon": "^9.2.4"
90
+ "react-portal": "^4.3.0"
94
91
  },
95
92
  "directories": {
96
93
  "test": "test"
@@ -9,13 +9,11 @@ import {
9
9
  FieldPathId,
10
10
  FieldPathList,
11
11
  FormContextType,
12
- GenericObjectType,
13
12
  getChangedFields,
14
13
  getTemplate,
15
14
  getUiOptions,
16
15
  isObject,
17
16
  mergeObjects,
18
- NAME_KEY,
19
17
  PathSchema,
20
18
  StrictRJSFSchema,
21
19
  Registry,
@@ -23,7 +21,6 @@ import {
23
21
  RegistryWidgetsType,
24
22
  RJSFSchema,
25
23
  RJSFValidationError,
26
- RJSF_ADDITIONAL_PROPERTIES_FLAG,
27
24
  SchemaUtilsType,
28
25
  shouldRender,
29
26
  SUBMIT_BTN_OPTIONS_KEY,
@@ -44,6 +41,8 @@ import {
44
41
  ERRORS_KEY,
45
42
  ID_KEY,
46
43
  NameGeneratorFunction,
44
+ getUsedFormData,
45
+ getFieldNames,
47
46
  } from '@rjsf/utils';
48
47
  import _cloneDeep from 'lodash/cloneDeep';
49
48
  import _get from 'lodash/get';
@@ -56,6 +55,11 @@ import _unset from 'lodash/unset';
56
55
  import getDefaultRegistry from '../getDefaultRegistry';
57
56
  import { ADDITIONAL_PROPERTY_KEY_REMOVE, IS_RESET } from './constants';
58
57
 
58
+ /** Represents a boolean option that is deprecated.
59
+ * @deprecated - In a future major release, this type will be removed
60
+ */
61
+ type DeprecatedBooleanOption = boolean;
62
+
59
63
  /** The properties that are passed to the `Form` */
60
64
  export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> {
61
65
  /** The JSON schema object for the form */
@@ -184,9 +188,9 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
184
188
  * is provided, then live validation will be performed when a field that was updated is blurred (as a performance
185
189
  * optimization).
186
190
  *
187
- * @deprecated - In a future major release, the `boolean` options for this flag will be removed
191
+ * NOTE: In a future major release, the `boolean` options for this flag will be removed
188
192
  */
189
- liveValidate?: boolean | 'onChange' | 'onBlur';
193
+ liveValidate?: 'onChange' | 'onBlur' | DeprecatedBooleanOption;
190
194
  /** Flag that describes when live omit will be performed. Live omit happens only when `omitExtraData` is also set to
191
195
  * to `true` and the form's data is updated by the user.
192
196
  *
@@ -195,9 +199,9 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
195
199
  * is provided, then live omit will be performed when a field that was updated is blurred (as a performance
196
200
  * optimization).
197
201
  *
198
- * @deprecated - In a future major release, the `boolean` options for this flag will be removed
202
+ * NOTE: In a future major release, the `boolean` options for this flag will be removed
199
203
  */
200
- liveOmit?: boolean | 'onChange' | 'onBlur';
204
+ liveOmit?: 'onChange' | 'onBlur' | DeprecatedBooleanOption;
201
205
  /** If set to true, then extra form data values that are not in any form field will be removed whenever `onSubmit` is
202
206
  * called. Set to `false` by default.
203
207
  */
@@ -360,6 +364,11 @@ export default class Form<
360
364
  */
361
365
  pendingChanges: PendingChange<T>[] = [];
362
366
 
367
+ /** Flag to track when we're processing a user-initiated field change.
368
+ * This prevents componentDidUpdate from reverting oneOf/anyOf option switches.
369
+ */
370
+ private _isProcessingUserChange = false;
371
+
363
372
  /** Constructs the `Form` from the `props`. Will setup the initial state from the props. It will also call the
364
373
  * `onChange` handler if the initially provided `formData` is modified to add missing default values as part of the
365
374
  * state construction.
@@ -454,11 +463,19 @@ export default class Form<
454
463
  ) {
455
464
  if (snapshot.shouldUpdate) {
456
465
  const { nextState } = snapshot;
457
- if (
458
- !deepEquals(nextState.formData, this.props.formData) &&
459
- !deepEquals(nextState.formData, prevState.formData) &&
460
- this.props.onChange
461
- ) {
466
+
467
+ // Prevent oneOf/anyOf option switches from reverting when getStateFromProps
468
+ // re-evaluates and produces stale formData.
469
+ const nextStateDiffersFromProps = !deepEquals(nextState.formData, this.props.formData);
470
+ const wasProcessingUserChange = this._isProcessingUserChange;
471
+ this._isProcessingUserChange = false;
472
+
473
+ if (wasProcessingUserChange && nextStateDiffersFromProps) {
474
+ // Skip - the user's option switch is already applied via processPendingChange
475
+ return;
476
+ }
477
+
478
+ if (nextStateDiffersFromProps && !deepEquals(nextState.formData, prevState.formData) && this.props.onChange) {
462
479
  this.props.onChange(toIChangeEvent(nextState));
463
480
  }
464
481
  this.setState(nextState);
@@ -752,73 +769,32 @@ export default class Form<
752
769
  *
753
770
  * @param formData - The data for the `Form`
754
771
  * @param fields - The fields to keep while filtering
772
+ * @deprecated - To be removed as an exported `Form` function in a future release; there isn't a planned replacement
755
773
  */
756
- getUsedFormData = (formData: T | undefined, fields: string[][]): T | undefined => {
757
- // For the case of a single input form
758
- if (fields.length === 0 && typeof formData !== 'object') {
759
- return formData;
760
- }
761
-
762
- // _pick has incorrect type definition, it works with string[][], because lodash/hasIn supports it
763
- const data: GenericObjectType = _pick(formData, fields as unknown as string[]);
764
- if (Array.isArray(formData)) {
765
- return Object.keys(data).map((key: string) => data[key]) as unknown as T;
766
- }
767
-
768
- return data as T;
774
+ getUsedFormData = (formData: T | undefined, fields: string[]): T | undefined => {
775
+ return getUsedFormData(formData, fields);
769
776
  };
770
777
 
771
778
  /** Returns the list of field names from inspecting the `pathSchema` as well as using the `formData`
772
779
  *
773
780
  * @param pathSchema - The `PathSchema` object for the form
774
781
  * @param [formData] - The form data to use while checking for empty objects/arrays
782
+ * @deprecated - To be removed as an exported `Form` function in a future release; there isn't a planned replacement
775
783
  */
776
784
  getFieldNames = (pathSchema: PathSchema<T>, formData?: T): string[][] => {
777
- const formValueHasData = (value: T, isLeaf: boolean) =>
778
- typeof value !== 'object' || _isEmpty(value) || (isLeaf && !_isEmpty(value));
779
- const getAllPaths = (_obj: GenericObjectType, acc: string[][] = [], paths: string[][] = [[]]) => {
780
- const objKeys = Object.keys(_obj);
781
- objKeys.forEach((key: string) => {
782
- const data = _obj[key];
783
- if (typeof data === 'object') {
784
- const newPaths = paths.map((path) => [...path, key]);
785
- // If an object is marked with additionalProperties, all its keys are valid
786
- if (data[RJSF_ADDITIONAL_PROPERTIES_FLAG] && data[NAME_KEY] !== '') {
787
- acc.push(data[NAME_KEY]);
788
- } else {
789
- getAllPaths(data, acc, newPaths);
790
- }
791
- } else if (key === NAME_KEY && data !== '') {
792
- paths.forEach((path) => {
793
- const formValue = _get(formData, path);
794
- const isLeaf = objKeys.length === 1;
795
- // adds path to fieldNames if it points to a value or an empty object/array which is not a leaf
796
- if (
797
- formValueHasData(formValue, isLeaf) ||
798
- (Array.isArray(formValue) && formValue.every((val) => formValueHasData(val, isLeaf)))
799
- ) {
800
- acc.push(path);
801
- }
802
- });
803
- }
804
- });
805
- return acc;
806
- };
807
-
808
- return getAllPaths(pathSchema);
785
+ return getFieldNames(pathSchema, formData);
809
786
  };
810
787
 
811
788
  /** Returns the `formData` after filtering to remove any extra data not in a form field
812
789
  *
813
790
  * @param formData - The data for the `Form`
814
791
  * @returns The `formData` after omitting extra data
792
+ * @deprecated - To be removed as an exported `Form` function in a future release, use `SchemaUtils.omitExtraData`
793
+ * instead.
815
794
  */
816
795
  omitExtraData = (formData?: T): T | undefined => {
817
796
  const { schema, schemaUtils } = this.state;
818
- const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);
819
- const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData);
820
- const fieldNames = this.getFieldNames(pathSchema, formData);
821
- return this.getUsedFormData(formData, fieldNames);
797
+ return schemaUtils.omitExtraData(schema, formData);
822
798
  };
823
799
 
824
800
  /** Allows a user to set a value for the provided `fieldPath`, which must be either a dotted path to the field OR a
@@ -863,6 +839,9 @@ export default class Form<
863
839
  if (this.pendingChanges.length === 0) {
864
840
  return;
865
841
  }
842
+ // Mark that we're processing a user-initiated change.
843
+ // This prevents componentDidUpdate from reverting oneOf/anyOf option switches.
844
+ this._isProcessingUserChange = true;
866
845
  const { newValue, path, id } = this.pendingChanges[0];
867
846
  const { newErrorSchema } = this.pendingChanges[0];
868
847
  const { extraErrors, omitExtraData, liveOmit, noValidate, liveValidate, onChange } = this.props;
@@ -873,6 +852,18 @@ export default class Form<
873
852
  const isRootPath = !path || path.length === 0 || (path.length === 1 && path[0] === rootPathId);
874
853
  let retrievedSchema = this.state.retrievedSchema;
875
854
  let formData = isRootPath ? newValue : _cloneDeep(oldFormData);
855
+
856
+ // When switching from null to an object option in oneOf, MultiSchemaField sends
857
+ // an object with property names but undefined values (e.g., {types: undefined, content: undefined}).
858
+ // In this case, pass undefined to getStateFromProps to trigger fresh default computation.
859
+ // Only do this when the previous formData was null/undefined (switching FROM null).
860
+ const hasOnlyUndefinedValues =
861
+ isObject(formData) &&
862
+ Object.keys(formData as object).length > 0 &&
863
+ Object.values(formData as object).every((v) => v === undefined);
864
+ const wasPreviouslyNull = oldFormData === null || oldFormData === undefined;
865
+ const inputForDefaults = hasOnlyUndefinedValues && wasPreviouslyNull ? undefined : formData;
866
+
876
867
  if (isObject(formData) || Array.isArray(formData)) {
877
868
  if (newValue === ADDITIONAL_PROPERTY_KEY_REMOVE) {
878
869
  // For additional properties, we were given the special remove this key value, so unset it
@@ -882,7 +873,7 @@ export default class Form<
882
873
  _set(formData, path, newValue);
883
874
  }
884
875
  // Pass true to skip live validation in `getStateFromProps()` since we will do it a bit later
885
- const newState = this.getStateFromProps(this.props, formData, undefined, undefined, undefined, true);
876
+ const newState = this.getStateFromProps(this.props, inputForDefaults, undefined, undefined, undefined, true);
886
877
  formData = newState.formData;
887
878
  retrievedSchema = newState.retrievedSchema;
888
879
  }
@@ -183,6 +183,7 @@ export default function LayoutMultiSchemaField<
183
183
 
184
184
  return (
185
185
  <FieldTemplate
186
+ fieldPathId={fieldPathId}
186
187
  id={id}
187
188
  schema={schema}
188
189
  label={(title || schema.title) ?? ''}
@@ -264,6 +264,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
264
264
  rawHelp: typeof help === 'string' ? help : undefined,
265
265
  errors: errorsComponent,
266
266
  rawErrors: hideError ? undefined : __errors,
267
+ fieldPathId,
267
268
  id,
268
269
  label,
269
270
  hidden,
@@ -1,4 +1,4 @@
1
- import { ChangeEvent, FocusEvent, useCallback } from 'react';
1
+ import { ChangeEvent, FocusEvent, MouseEvent, useCallback } from 'react';
2
2
  import {
3
3
  ariaDescribedByIds,
4
4
  BaseInputTemplateProps,
@@ -42,6 +42,7 @@ export default function BaseInputTemplate<
42
42
  hideError, // remove this from ...rest
43
43
  ...rest
44
44
  } = props;
45
+ const { ClearButton } = registry.templates.ButtonTemplates;
45
46
 
46
47
  // Note: since React 15.2.0 we can't forward unknown element attributes, so we
47
48
  // exclude the "options" and "schema" ones here.
@@ -73,6 +74,14 @@ export default function BaseInputTemplate<
73
74
  ({ target }: FocusEvent<HTMLInputElement>) => onFocus(id, target && target.value),
74
75
  [onFocus, id],
75
76
  );
77
+ const _onClear = useCallback(
78
+ (e: MouseEvent) => {
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ onChange(options.emptyValue ?? '');
82
+ },
83
+ [onChange, options.emptyValue],
84
+ );
76
85
 
77
86
  return (
78
87
  <>
@@ -91,6 +100,9 @@ export default function BaseInputTemplate<
91
100
  onFocus={_onFocus}
92
101
  aria-describedby={ariaDescribedByIds(id, !!schema.examples)}
93
102
  />
103
+ {options.allowClearTextInputs && !readonly && !disabled && inputValue && (
104
+ <ClearButton registry={registry} onClick={_onClear} />
105
+ )}
94
106
  {Array.isArray(schema.examples) && (
95
107
  <datalist key={`datalist_${id}`} id={examplesId(id)}>
96
108
  {(schema.examples as string[])
@@ -48,3 +48,27 @@ export function RemoveButton<T = any, S extends StrictRJSFSchema = RJSFSchema, F
48
48
  <IconButton title={translateString(TranslatableString.RemoveButton)} {...props} iconType='danger' icon='remove' />
49
49
  );
50
50
  }
51
+
52
+ export function ClearButton<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>({
53
+ id,
54
+ className,
55
+ onClick,
56
+ disabled,
57
+ registry,
58
+ ...props
59
+ }: IconButtonProps<T, S, F>) {
60
+ const { translateString } = registry;
61
+ return (
62
+ <IconButton
63
+ id={id}
64
+ iconType='default'
65
+ icon='remove'
66
+ className='btn-clear col-xs-12'
67
+ title={translateString(TranslatableString.ClearButton)}
68
+ onClick={onClick}
69
+ disabled={disabled}
70
+ registry={registry}
71
+ {...props}
72
+ />
73
+ );
74
+ }
@@ -2,7 +2,7 @@ import { FormContextType, RJSFSchema, StrictRJSFSchema, TemplatesType } from '@r
2
2
 
3
3
  import SubmitButton from './SubmitButton';
4
4
  import AddButton from './AddButton';
5
- import { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } from './IconButton';
5
+ import { CopyButton, MoveDownButton, MoveUpButton, RemoveButton, ClearButton } from './IconButton';
6
6
 
7
7
  function buttonTemplates<
8
8
  T = any,
@@ -16,6 +16,7 @@ function buttonTemplates<
16
16
  MoveDownButton,
17
17
  MoveUpButton,
18
18
  RemoveButton,
19
+ ClearButton,
19
20
  };
20
21
  }
21
22
 
@@ -46,6 +46,15 @@ export default function ObjectFieldTemplate<
46
46
  registry,
47
47
  options,
48
48
  );
49
+
50
+ // For "pure union" schemas (oneOf/anyOf without properties), skip rendering the empty fieldset wrapper.
51
+ // The AnyOfField/OneOfField will handle rendering the union selector and selected variant's content directly.
52
+ const isPureUnionSchema = (schema.oneOf || schema.anyOf) && !schema.properties && properties.length === 0;
53
+
54
+ if (isPureUnionSchema) {
55
+ return null;
56
+ }
57
+
49
58
  const showOptionalDataControlInTitle = !readonly && !disabled;
50
59
  // Button templates are not overridden in the uiSchema
51
60
  const {
@@ -54,9 +54,4 @@ function TextareaWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
54
54
  );
55
55
  }
56
56
 
57
- TextareaWidget.defaultProps = {
58
- autofocus: false,
59
- options: {},
60
- };
61
-
62
57
  export default TextareaWidget;