@rjsf/core 6.6.2 → 6.7.1

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 (162) hide show
  1. package/LICENSE.md +201 -0
  2. package/dist/core.umd.js +312 -252
  3. package/dist/index.cjs +947 -878
  4. package/dist/index.cjs.map +4 -4
  5. package/dist/index.esm.js +741 -667
  6. package/dist/index.esm.js.map +4 -4
  7. package/lib/components/Form.d.ts +2 -2
  8. package/lib/components/Form.d.ts.map +1 -1
  9. package/lib/components/Form.js +30 -15
  10. package/lib/components/RichDescription.d.ts +1 -1
  11. package/lib/components/RichDescription.d.ts.map +1 -1
  12. package/lib/components/RichHelp.d.ts +1 -1
  13. package/lib/components/RichHelp.d.ts.map +1 -1
  14. package/lib/components/SchemaExamples.d.ts +1 -1
  15. package/lib/components/SchemaExamples.d.ts.map +1 -1
  16. package/lib/components/fields/ArrayField.d.ts +1 -1
  17. package/lib/components/fields/ArrayField.d.ts.map +1 -1
  18. package/lib/components/fields/BooleanField.d.ts +1 -1
  19. package/lib/components/fields/BooleanField.d.ts.map +1 -1
  20. package/lib/components/fields/CyclicSchemaField.d.ts +8 -0
  21. package/lib/components/fields/CyclicSchemaField.d.ts.map +1 -0
  22. package/lib/components/fields/CyclicSchemaField.js +21 -0
  23. package/lib/components/fields/FallbackField.d.ts +1 -1
  24. package/lib/components/fields/FallbackField.d.ts.map +1 -1
  25. package/lib/components/fields/LayoutGridField.d.ts +1 -1
  26. package/lib/components/fields/LayoutGridField.d.ts.map +1 -1
  27. package/lib/components/fields/LayoutHeaderField.d.ts +1 -1
  28. package/lib/components/fields/LayoutHeaderField.d.ts.map +1 -1
  29. package/lib/components/fields/LayoutMultiSchemaField.d.ts +1 -1
  30. package/lib/components/fields/LayoutMultiSchemaField.d.ts.map +1 -1
  31. package/lib/components/fields/MultiSchemaField.d.ts +1 -46
  32. package/lib/components/fields/MultiSchemaField.d.ts.map +1 -1
  33. package/lib/components/fields/MultiSchemaField.js +111 -135
  34. package/lib/components/fields/NumberField.d.ts +1 -1
  35. package/lib/components/fields/NumberField.d.ts.map +1 -1
  36. package/lib/components/fields/NumberField.js +26 -15
  37. package/lib/components/fields/ObjectField.d.ts +1 -1
  38. package/lib/components/fields/ObjectField.d.ts.map +1 -1
  39. package/lib/components/fields/ObjectField.js +20 -5
  40. package/lib/components/fields/OptionalDataControlsField.d.ts +1 -1
  41. package/lib/components/fields/OptionalDataControlsField.d.ts.map +1 -1
  42. package/lib/components/fields/SchemaField.d.ts +13 -7
  43. package/lib/components/fields/SchemaField.d.ts.map +1 -1
  44. package/lib/components/fields/SchemaField.js +37 -24
  45. package/lib/components/fields/StringField.d.ts +1 -1
  46. package/lib/components/fields/StringField.d.ts.map +1 -1
  47. package/lib/components/fields/index.d.ts.map +1 -1
  48. package/lib/components/fields/index.js +2 -0
  49. package/lib/components/templates/ArrayFieldDescriptionTemplate.d.ts +1 -1
  50. package/lib/components/templates/ArrayFieldDescriptionTemplate.d.ts.map +1 -1
  51. package/lib/components/templates/ArrayFieldItemButtonsTemplate.d.ts +1 -1
  52. package/lib/components/templates/ArrayFieldItemButtonsTemplate.d.ts.map +1 -1
  53. package/lib/components/templates/ArrayFieldItemTemplate.d.ts +1 -1
  54. package/lib/components/templates/ArrayFieldItemTemplate.d.ts.map +1 -1
  55. package/lib/components/templates/ArrayFieldTemplate.d.ts +1 -1
  56. package/lib/components/templates/ArrayFieldTemplate.d.ts.map +1 -1
  57. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts +1 -1
  58. package/lib/components/templates/ArrayFieldTitleTemplate.d.ts.map +1 -1
  59. package/lib/components/templates/BaseInputTemplate.d.ts +1 -1
  60. package/lib/components/templates/BaseInputTemplate.d.ts.map +1 -1
  61. package/lib/components/templates/BaseInputTemplate.js +2 -2
  62. package/lib/components/templates/ButtonTemplates/AddButton.d.ts +1 -1
  63. package/lib/components/templates/ButtonTemplates/AddButton.d.ts.map +1 -1
  64. package/lib/components/templates/ButtonTemplates/IconButton.d.ts +6 -6
  65. package/lib/components/templates/ButtonTemplates/IconButton.d.ts.map +1 -1
  66. package/lib/components/templates/ButtonTemplates/SubmitButton.d.ts +1 -1
  67. package/lib/components/templates/ButtonTemplates/SubmitButton.d.ts.map +1 -1
  68. package/lib/components/templates/CyclicSchemaExpandTemplate.d.ts +7 -0
  69. package/lib/components/templates/CyclicSchemaExpandTemplate.d.ts.map +1 -0
  70. package/lib/components/templates/CyclicSchemaExpandTemplate.js +12 -0
  71. package/lib/components/templates/DescriptionField.d.ts +1 -1
  72. package/lib/components/templates/DescriptionField.d.ts.map +1 -1
  73. package/lib/components/templates/ErrorList.d.ts +1 -1
  74. package/lib/components/templates/ErrorList.d.ts.map +1 -1
  75. package/lib/components/templates/FallbackFieldTemplate.d.ts +1 -1
  76. package/lib/components/templates/FallbackFieldTemplate.d.ts.map +1 -1
  77. package/lib/components/templates/FieldErrorTemplate.d.ts +1 -1
  78. package/lib/components/templates/FieldErrorTemplate.d.ts.map +1 -1
  79. package/lib/components/templates/FieldHelpTemplate.d.ts +1 -1
  80. package/lib/components/templates/FieldHelpTemplate.d.ts.map +1 -1
  81. package/lib/components/templates/FieldTemplate/FieldTemplate.d.ts +1 -1
  82. package/lib/components/templates/FieldTemplate/FieldTemplate.d.ts.map +1 -1
  83. package/lib/components/templates/FieldTemplate/Label.d.ts +1 -1
  84. package/lib/components/templates/FieldTemplate/Label.d.ts.map +1 -1
  85. package/lib/components/templates/GridTemplate.d.ts +1 -1
  86. package/lib/components/templates/GridTemplate.d.ts.map +1 -1
  87. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts +1 -1
  88. package/lib/components/templates/MultiSchemaFieldTemplate.d.ts.map +1 -1
  89. package/lib/components/templates/ObjectFieldTemplate.d.ts +1 -1
  90. package/lib/components/templates/ObjectFieldTemplate.d.ts.map +1 -1
  91. package/lib/components/templates/OptionalDataControlsTemplate.d.ts +1 -1
  92. package/lib/components/templates/OptionalDataControlsTemplate.d.ts.map +1 -1
  93. package/lib/components/templates/TitleField.d.ts +1 -1
  94. package/lib/components/templates/TitleField.d.ts.map +1 -1
  95. package/lib/components/templates/UnsupportedField.d.ts +1 -1
  96. package/lib/components/templates/UnsupportedField.d.ts.map +1 -1
  97. package/lib/components/templates/UnsupportedField.js +1 -1
  98. package/lib/components/templates/WrapIfAdditionalTemplate.d.ts +1 -1
  99. package/lib/components/templates/WrapIfAdditionalTemplate.d.ts.map +1 -1
  100. package/lib/components/templates/index.d.ts.map +1 -1
  101. package/lib/components/templates/index.js +2 -0
  102. package/lib/components/widgets/AltDateTimeWidget.d.ts +1 -1
  103. package/lib/components/widgets/AltDateTimeWidget.d.ts.map +1 -1
  104. package/lib/components/widgets/AltDateWidget.d.ts +1 -1
  105. package/lib/components/widgets/AltDateWidget.d.ts.map +1 -1
  106. package/lib/components/widgets/CheckboxWidget.d.ts +1 -1
  107. package/lib/components/widgets/CheckboxWidget.d.ts.map +1 -1
  108. package/lib/components/widgets/CheckboxesWidget.d.ts +1 -1
  109. package/lib/components/widgets/CheckboxesWidget.d.ts.map +1 -1
  110. package/lib/components/widgets/CheckboxesWidget.js +2 -2
  111. package/lib/components/widgets/ColorWidget.d.ts +1 -1
  112. package/lib/components/widgets/ColorWidget.d.ts.map +1 -1
  113. package/lib/components/widgets/DateTimeWidget.d.ts +1 -1
  114. package/lib/components/widgets/DateTimeWidget.d.ts.map +1 -1
  115. package/lib/components/widgets/DateWidget.d.ts +1 -1
  116. package/lib/components/widgets/DateWidget.d.ts.map +1 -1
  117. package/lib/components/widgets/EmailWidget.d.ts +1 -1
  118. package/lib/components/widgets/EmailWidget.d.ts.map +1 -1
  119. package/lib/components/widgets/FileWidget.d.ts +1 -1
  120. package/lib/components/widgets/FileWidget.d.ts.map +1 -1
  121. package/lib/components/widgets/HiddenWidget.d.ts +1 -1
  122. package/lib/components/widgets/HiddenWidget.d.ts.map +1 -1
  123. package/lib/components/widgets/PasswordWidget.d.ts +1 -1
  124. package/lib/components/widgets/PasswordWidget.d.ts.map +1 -1
  125. package/lib/components/widgets/RadioWidget.d.ts +1 -1
  126. package/lib/components/widgets/RadioWidget.d.ts.map +1 -1
  127. package/lib/components/widgets/RadioWidget.js +2 -2
  128. package/lib/components/widgets/RangeWidget.d.ts +1 -1
  129. package/lib/components/widgets/RangeWidget.d.ts.map +1 -1
  130. package/lib/components/widgets/RatingWidget.d.ts +1 -1
  131. package/lib/components/widgets/RatingWidget.d.ts.map +1 -1
  132. package/lib/components/widgets/SelectWidget.d.ts +1 -1
  133. package/lib/components/widgets/SelectWidget.d.ts.map +1 -1
  134. package/lib/components/widgets/SelectWidget.js +9 -8
  135. package/lib/components/widgets/TextWidget.d.ts +1 -1
  136. package/lib/components/widgets/TextWidget.d.ts.map +1 -1
  137. package/lib/components/widgets/TextareaWidget.d.ts +1 -1
  138. package/lib/components/widgets/TextareaWidget.d.ts.map +1 -1
  139. package/lib/components/widgets/TextareaWidget.js +3 -3
  140. package/lib/components/widgets/TimeWidget.d.ts +1 -1
  141. package/lib/components/widgets/TimeWidget.d.ts.map +1 -1
  142. package/lib/components/widgets/URLWidget.d.ts +1 -1
  143. package/lib/components/widgets/URLWidget.d.ts.map +1 -1
  144. package/lib/components/widgets/UpDownWidget.d.ts +1 -1
  145. package/lib/components/widgets/UpDownWidget.d.ts.map +1 -1
  146. package/lib/tsconfig.tsbuildinfo +1 -1
  147. package/package.json +28 -27
  148. package/src/components/Form.tsx +34 -15
  149. package/src/components/fields/CyclicSchemaField.tsx +40 -0
  150. package/src/components/fields/MultiSchemaField.tsx +201 -230
  151. package/src/components/fields/NumberField.tsx +30 -16
  152. package/src/components/fields/ObjectField.tsx +29 -5
  153. package/src/components/fields/SchemaField.tsx +46 -34
  154. package/src/components/fields/index.ts +2 -0
  155. package/src/components/templates/BaseInputTemplate.tsx +2 -5
  156. package/src/components/templates/CyclicSchemaExpandTemplate.tsx +31 -0
  157. package/src/components/templates/UnsupportedField.tsx +1 -1
  158. package/src/components/templates/index.ts +2 -0
  159. package/src/components/widgets/CheckboxesWidget.tsx +2 -2
  160. package/src/components/widgets/RadioWidget.tsx +2 -2
  161. package/src/components/widgets/SelectWidget.tsx +45 -30
  162. package/src/components/widgets/TextareaWidget.tsx +3 -3
@@ -0,0 +1,40 @@
1
+ import { useState } from 'react';
2
+ import type { FieldProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils';
3
+ import { getTemplate, getUiOptions, RJSF_REF_CYCLE_KEY } from '@rjsf/utils';
4
+
5
+ /** The `CyclicSchemaField` component is used to render a field in the schema that is marked with RJSF_REF_CYCLE_KEY ===
6
+ * true
7
+ *
8
+ * @param props - The `FieldProps` for this template
9
+ */
10
+ export default function CyclicSchemaField<
11
+ T = any,
12
+ S extends StrictRJSFSchema = RJSFSchema,
13
+ F extends FormContextType = any,
14
+ >(props: FieldProps<T, S, F>) {
15
+ const [expanded, setExpanded] = useState(false);
16
+ const { name, registry, schema, uiSchema, fieldPathId } = props;
17
+ const { globalUiOptions } = registry;
18
+
19
+ const uiOptions = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
20
+ const CyclicSchemaExpandTemplate = getTemplate<'CyclicSchemaExpandTemplate', T, S, F>(
21
+ 'CyclicSchemaExpandTemplate',
22
+ registry,
23
+ uiOptions,
24
+ );
25
+ if (!expanded) {
26
+ return (
27
+ <CyclicSchemaExpandTemplate
28
+ registry={registry}
29
+ schema={schema}
30
+ uiSchema={uiSchema}
31
+ name={name}
32
+ fieldPathId={fieldPathId}
33
+ onExpand={() => setExpanded(true)}
34
+ />
35
+ );
36
+ }
37
+ const { fields } = registry;
38
+ const { SchemaField } = fields;
39
+ return <SchemaField {...props} schema={{ ...schema, [RJSF_REF_CYCLE_KEY]: false }} />;
40
+ }
@@ -1,4 +1,4 @@
1
- import { Component } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { FieldProps, FormContextType, RJSFSchema, StrictRJSFSchema, UiSchema } from '@rjsf/utils';
3
3
  import {
4
4
  ANY_OF_KEY,
@@ -8,6 +8,7 @@ import {
8
8
  getTemplate,
9
9
  getUiOptions,
10
10
  getWidget,
11
+ hashObject,
11
12
  isFormDataAvailable,
12
13
  mergeSchemas,
13
14
  ONE_OF_KEY,
@@ -18,102 +19,86 @@ import get from 'lodash/get';
18
19
  import isEmpty from 'lodash/isEmpty';
19
20
  import omit from 'lodash/omit';
20
21
 
21
- /** Type used for the state of the `AnyOfField` component */
22
- interface AnyOfFieldState<S extends StrictRJSFSchema = RJSFSchema> {
23
- /** The currently selected option */
24
- selectedOption: number;
25
- /** The option schemas after retrieving all $refs */
26
- retrievedOptions: S[];
27
- }
28
-
29
22
  /** The `AnyOfField` component is used to render a field in the schema that is an `anyOf`, `allOf` or `oneOf`. It tracks
30
23
  * the currently selected option and cleans up any irrelevant data in `formData`.
31
24
  *
32
25
  * @param props - The `FieldProps` for this template
33
26
  */
34
- class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> extends Component<
35
- FieldProps<T, S, F>,
36
- AnyOfFieldState<S>
37
- > {
38
- /** Flag to skip the formData-change-driven option recalculation when the user just selected an option.
39
- * Set to true in the setState callback of onOptionChange (after onChange is called), consumed and reset in
40
- * componentDidUpdate. This prevents the matching-option recalculation from overriding a user's explicit choice
41
- * when getDefaultFormState populates undefined properties that make deepEquals see a false formData change.
42
- */
43
- private skipNextOptionRecalculation = false;
44
- /** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state
45
- *
46
- * @param props - The `FieldProps` for this template
47
- */
48
- constructor(props: FieldProps<T, S, F>) {
49
- super(props);
27
+ function AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
28
+ props: FieldProps<T, S, F>,
29
+ ) {
30
+ const {
31
+ name,
32
+ disabled = false,
33
+ errorSchema = {},
34
+ formData,
35
+ fieldPathId,
36
+ onBlur,
37
+ onChange,
38
+ onFocus,
39
+ options,
40
+ readonly,
41
+ registry,
42
+ required = false,
43
+ schema,
44
+ uiSchema,
45
+ } = props;
46
+ const { schemaUtils } = registry;
50
47
 
51
- const {
52
- formData,
53
- options,
54
- registry: { schemaUtils },
55
- } = this.props;
56
- // cache the retrieved options in state in case they have $refs to save doing it later
57
- const retrievedOptions = options.map((opt: S) => schemaUtils.retrieveSchema(opt, formData));
48
+ // Hash formData by value so the memo only invalidates when data actually changes, not on every
49
+ // new object reference. hashObject(undefined) throws, so null is used as the fallback.
50
+ const formDataHash = hashObject(formData ?? null);
58
51
 
59
- this.state = {
60
- retrievedOptions,
61
- selectedOption: this.getMatchingOption(0, formData, retrievedOptions),
62
- };
63
- }
52
+ // retrievedOptions is purely derived from options — useMemo handles re-derivation automatically
53
+ // when options, schemaUtils, or formData's value changes, with no render-phase dispatch needed.
54
+ const retrievedOptions = useMemo(
55
+ () => options.map((opt: S) => schemaUtils.retrieveSchema(opt, formData)),
56
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- formDataHash is the value-stable proxy for formData
57
+ [options, schemaUtils, formDataHash],
58
+ );
64
59
 
65
- /** React lifecycle method that is called when the props and/or state for this component is updated. It recomputes the
66
- * currently selected option based on the overall `formData`
67
- *
68
- * @param prevProps - The previous `FieldProps` for this template
69
- * @param prevState - The previous `AnyOfFieldState` for this template
60
+ const [selectedOption, setSelectedOption] = useState<number>(() => {
61
+ const discriminator = getDiscriminatorFieldFromSchema<S>(schema);
62
+ return schemaUtils.getClosestMatchingOption(formData, retrievedOptions, 0, discriminator);
63
+ });
64
+
65
+ /** Flag to skip the formData-change-driven option recalculation when the user just selected an option.
66
+ * Set to true in onOptionChange (before onChange is called), consumed and reset in the update effect.
67
+ * This prevents the matching-option recalculation from overriding a user's explicit choice when
68
+ * getDefaultFormState populates undefined properties that make deepEquals see a false formData change.
70
69
  */
71
- componentDidUpdate(prevProps: Readonly<FieldProps<T, S, F>>, prevState: Readonly<AnyOfFieldState>) {
72
- const { formData, options, fieldPathId } = this.props;
73
- const { selectedOption } = this.state;
74
- let newState = this.state;
75
- if (!deepEquals(prevProps.options, options)) {
76
- const {
77
- registry: { schemaUtils },
78
- } = this.props;
79
- // re-cache the retrieved options in state in case they have $refs to save doing it later
80
- const retrievedOptions = options.map((opt: S) => schemaUtils.retrieveSchema(opt, formData));
81
- newState = { selectedOption, retrievedOptions };
82
- }
83
- if (!deepEquals(formData, prevProps.formData) && fieldPathId.$id === prevProps.fieldPathId.$id) {
84
- if (this.skipNextOptionRecalculation) {
85
- this.skipNextOptionRecalculation = false;
86
- } else {
87
- const { retrievedOptions } = newState;
88
- const matchingOption = this.getMatchingOption(selectedOption, formData, retrievedOptions);
70
+ const skipNextOptionRecalculation = useRef(false);
71
+ const prevFormDataRef = useRef<T | undefined>(formData);
72
+ const prevFieldIdRef = useRef(fieldPathId.$id);
73
+
74
+ // Mirrors componentDidUpdate: re-match selectedOption when formData changes on the same field.
75
+ // Runs after every render (no deps array) to compare against prev values stored in refs.
76
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
77
+ useEffect(() => {
78
+ const prevFormData = prevFormDataRef.current;
79
+ const prevFieldId = prevFieldIdRef.current;
80
+ prevFormDataRef.current = formData;
81
+ prevFieldIdRef.current = fieldPathId.$id;
89
82
 
90
- if (prevState && matchingOption !== selectedOption) {
91
- newState = { selectedOption: matchingOption, retrievedOptions };
92
- }
83
+ if (!deepEquals(formData, prevFormData) && fieldPathId.$id === prevFieldId) {
84
+ if (skipNextOptionRecalculation.current) {
85
+ skipNextOptionRecalculation.current = false;
86
+ return;
87
+ }
88
+ const discriminator = getDiscriminatorFieldFromSchema<S>(schema);
89
+ const matchingOption = schemaUtils.getClosestMatchingOption(
90
+ formData,
91
+ retrievedOptions,
92
+ selectedOption,
93
+ discriminator,
94
+ );
95
+ if (matchingOption !== selectedOption) {
96
+ setSelectedOption(matchingOption);
93
97
  }
94
98
  }
95
- if (newState !== this.state) {
96
- // oxlint-disable-next-line react/no-did-update-set-state -- guarded to prevent infinite loop
97
- this.setState(newState);
98
- }
99
- }
99
+ });
100
100
 
101
- /** Determines the best matching option for the given `formData` and `options`.
102
- *
103
- * @param formData - The new formData
104
- * @param options - The list of options to choose from
105
- * @return - The index of the `option` that best matches the `formData`
106
- */
107
- getMatchingOption(selectedOption: number, formData: T | undefined, options: S[]) {
108
- const {
109
- schema,
110
- registry: { schemaUtils },
111
- } = this.props;
112
-
113
- const discriminator = getDiscriminatorFieldFromSchema<S>(schema);
114
- const option = schemaUtils.getClosestMatchingOption(formData, options, selectedOption, discriminator);
115
- return option;
116
- }
101
+ const fieldId = `${fieldPathId.$id}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`;
117
102
 
118
103
  /** Callback handler to remember what the currently selected option is. In addition to that the `formData` is updated
119
104
  * to remove properties that are not part of the newly selected option schema, and then the updated data is passed to
@@ -121,166 +106,152 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
121
106
  *
122
107
  * @param option - The new option value being selected
123
108
  */
124
- onOptionChange = (option?: string) => {
125
- const { selectedOption, retrievedOptions } = this.state;
126
- const { disabled = false, formData, onChange, readonly = false, registry, fieldPathId } = this.props;
127
- if (disabled || readonly) {
128
- return;
129
- }
130
- const { schemaUtils } = registry;
131
- const intOption = option !== undefined ? parseInt(option, 10) : -1;
132
- if (intOption === selectedOption) {
133
- return;
134
- }
135
- const newOption = intOption >= 0 ? retrievedOptions[intOption] : undefined;
136
- const oldOption = selectedOption >= 0 ? retrievedOptions[selectedOption] : undefined;
137
-
138
- let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData);
139
- if (newOption) {
140
- // Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
141
- // so that only the root objects themselves are created without adding undefined children properties
142
- newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, 'excludeObjectChildren') as T;
143
- }
144
-
145
- this.setState({ selectedOption: intOption }, () => {
146
- this.skipNextOptionRecalculation = true;
147
- onChange(newFormData, fieldPathId.path, undefined, this.getFieldId());
148
- });
149
- };
109
+ const onOptionChange = useCallback(
110
+ (option?: string) => {
111
+ if (disabled || readonly) {
112
+ return;
113
+ }
114
+ const intOption = option !== undefined ? parseInt(option, 10) : -1;
115
+ if (intOption === selectedOption) {
116
+ return;
117
+ }
118
+ const newOption = intOption >= 0 ? retrievedOptions[intOption] : undefined;
119
+ const oldOption = selectedOption >= 0 ? retrievedOptions[selectedOption] : undefined;
150
120
 
151
- getFieldId() {
152
- const { fieldPathId, schema } = this.props;
153
- return `${fieldPathId.$id}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`;
154
- }
121
+ let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData);
122
+ if (newOption) {
123
+ // Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
124
+ // so that only the root objects themselves are created without adding undefined children properties
125
+ newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, 'excludeObjectChildren') as T;
126
+ }
155
127
 
156
- /** Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData`
157
- */
158
- render() {
159
- const {
160
- name,
161
- disabled = false,
162
- errorSchema = {},
163
- formData,
164
- onBlur,
165
- onFocus,
166
- readonly,
167
- required = false,
168
- registry,
169
- schema,
170
- uiSchema,
171
- } = this.props;
128
+ setSelectedOption(intOption);
129
+ skipNextOptionRecalculation.current = true;
130
+ onChange(newFormData, fieldPathId.path, undefined, fieldId);
131
+ },
132
+ // setSelectedOption is stable (guaranteed by useState); skipNextOptionRecalculation is a ref
133
+ [selectedOption, retrievedOptions, disabled, readonly, schemaUtils, formData, fieldPathId, onChange, fieldId],
134
+ );
172
135
 
173
- const { widgets, fields, translateString, globalUiOptions, schemaUtils } = registry;
174
- const { SchemaField: SchemaFieldComponent } = fields;
175
- const MultiSchemaFieldTemplate = getTemplate<'MultiSchemaFieldTemplate', T, S, F>(
176
- 'MultiSchemaFieldTemplate',
177
- registry,
178
- globalUiOptions,
179
- );
180
- const isOptionalRender = shouldRenderOptionalField<T, S, F>(registry, schema, required, uiSchema);
181
- const hasFormData = isFormDataAvailable<T>(formData);
136
+ const { widgets, fields, translateString, globalUiOptions } = registry;
137
+ const { SchemaField: SchemaFieldComponent } = fields;
138
+ const MultiSchemaFieldTemplate = getTemplate<'MultiSchemaFieldTemplate', T, S, F>(
139
+ 'MultiSchemaFieldTemplate',
140
+ registry,
141
+ globalUiOptions,
142
+ );
143
+ const isOptionalRender = shouldRenderOptionalField<T, S, F>(registry, schema, required, uiSchema);
144
+ const hasFormData = isFormDataAvailable<T>(formData);
182
145
 
183
- const { selectedOption, retrievedOptions } = this.state;
184
- const {
185
- widget = 'select',
186
- placeholder,
187
- autofocus,
188
- autocomplete,
189
- title = schema.title,
190
- ...uiOptions
191
- } = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
192
- const Widget = getWidget<T, S, F>({ type: 'number' }, widget, widgets);
193
- const rawErrors = get(errorSchema, ERRORS_KEY, []);
194
- const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
195
- const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
146
+ const {
147
+ widget = 'select',
148
+ placeholder,
149
+ autofocus,
150
+ autocomplete,
151
+ title = schema.title,
152
+ ...uiOptions
153
+ } = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
154
+ const Widget = getWidget<T, S, F>({ type: 'number' }, widget, widgets);
155
+ const rawErrors = get(errorSchema, ERRORS_KEY, []);
156
+ const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
157
+ const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
196
158
 
197
- const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null;
198
- let optionSchema: S | undefined | null;
159
+ const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null;
160
+ let optionSchema: S | undefined | null;
199
161
 
200
- if (option) {
201
- // merge top level required field
202
- const { required: schemaRequired } = schema;
203
- // Merge in all the non-oneOf/anyOf properties and also skip the special ADDITIONAL_PROPERTY_FLAG property
204
- optionSchema = schemaRequired ? (mergeSchemas({ required: schemaRequired }, option) as S) : option;
162
+ if (option) {
163
+ const { required: schemaRequired, type: schemaType } = schema;
164
+ const parentProps: Partial<S> = {};
165
+ if (schemaRequired) {
166
+ parentProps.required = schemaRequired as S['required'];
167
+ }
168
+ // Propagate the parent schema type to options that don't define their own.
169
+ // This is necessary when the parent constrains the type (e.g. { type: 'string',
170
+ // oneOf: [{ pattern: '...' }, { pattern: '...' }] }) but the option sub-schemas
171
+ // omit the type — without it, getSchemaType returns undefined and the option
172
+ // renders as FallbackField instead of the correct widget (e.g. StringField).
173
+ if (schemaType !== undefined && !('type' in option)) {
174
+ parentProps.type = schemaType;
205
175
  }
176
+ // Merge in all the non-oneOf/anyOf properties and also skip the special ADDITIONAL_PROPERTY_FLAG property
177
+ optionSchema = Object.keys(parentProps).length > 0 ? (mergeSchemas(parentProps, option) as S) : option;
178
+ }
206
179
 
207
- // First we will check to see if there is an anyOf/oneOf override for the UI schema
208
- let optionsUiSchema: UiSchema<T, S, F>[] = [];
209
- if (ONE_OF_KEY in schema && uiSchema && ONE_OF_KEY in uiSchema) {
210
- if (Array.isArray(uiSchema[ONE_OF_KEY])) {
211
- optionsUiSchema = uiSchema[ONE_OF_KEY];
212
- } else {
213
- // oxlint-disable-next-line no-console
214
- console.warn(`uiSchema.oneOf is not an array for "${title || name}"`);
215
- }
216
- } else if (ANY_OF_KEY in schema && uiSchema && ANY_OF_KEY in uiSchema) {
217
- if (Array.isArray(uiSchema[ANY_OF_KEY])) {
218
- optionsUiSchema = uiSchema[ANY_OF_KEY];
219
- } else {
220
- // oxlint-disable-next-line no-console
221
- console.warn(`uiSchema.anyOf is not an array for "${title || name}"`);
222
- }
180
+ // First we will check to see if there is an anyOf/oneOf override for the UI schema
181
+ let optionsUiSchema: UiSchema<T, S, F>[] = [];
182
+ if (ONE_OF_KEY in schema && uiSchema && ONE_OF_KEY in uiSchema) {
183
+ if (Array.isArray(uiSchema[ONE_OF_KEY])) {
184
+ optionsUiSchema = uiSchema[ONE_OF_KEY];
185
+ } else {
186
+ // oxlint-disable-next-line no-console
187
+ console.warn(`uiSchema.oneOf is not an array for "${title || name}"`);
223
188
  }
224
- // Then we pick the one that matches the selected option index, if one exists otherwise default to the main uiSchema
225
- let optionUiSchema = uiSchema;
226
- if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) {
227
- optionUiSchema = optionsUiSchema[selectedOption];
189
+ } else if (ANY_OF_KEY in schema && uiSchema && ANY_OF_KEY in uiSchema) {
190
+ if (Array.isArray(uiSchema[ANY_OF_KEY])) {
191
+ optionsUiSchema = uiSchema[ANY_OF_KEY];
192
+ } else {
193
+ // oxlint-disable-next-line no-console
194
+ console.warn(`uiSchema.anyOf is not an array for "${title || name}"`);
228
195
  }
196
+ }
197
+ // Then we pick the one that matches the selected option index, if one exists otherwise default to the main uiSchema
198
+ let optionUiSchema = uiSchema;
199
+ if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) {
200
+ optionUiSchema = optionsUiSchema[selectedOption];
201
+ }
229
202
 
230
- const translateEnum: TranslatableString = title
231
- ? TranslatableString.TitleOptionPrefix
232
- : TranslatableString.OptionPrefix;
233
- const translateParams = title ? [title] : [];
234
- const enumOptions = retrievedOptions.map((opt: { title?: string }, index: number) => {
235
- // Also see if there is an override title in the uiSchema for each option, otherwise use the title from the option
236
- const { title: uiTitle = opt.title } = getUiOptions<T, S, F>(optionsUiSchema[index]);
237
- return {
238
- label: uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))),
239
- value: index,
240
- };
241
- });
242
-
243
- const selector =
244
- !isOptionalRender || hasFormData ? (
245
- <Widget
246
- id={this.getFieldId()}
247
- name={`${name}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
248
- schema={{ type: 'number', default: 0 } as S}
249
- onChange={this.onOptionChange}
250
- onBlur={onBlur}
251
- onFocus={onFocus}
252
- disabled={disabled || isEmpty(enumOptions)}
253
- multiple={false}
254
- rawErrors={rawErrors}
255
- errorSchema={fieldErrorSchema}
256
- value={selectedOption >= 0 ? selectedOption : undefined}
257
- options={{ enumOptions, ...uiOptions }}
258
- registry={registry}
259
- placeholder={placeholder}
260
- autocomplete={autocomplete}
261
- autofocus={autofocus}
262
- label={title ?? name}
263
- hideLabel={!displayLabel}
264
- readonly={readonly}
265
- />
266
- ) : undefined;
267
-
268
- const optionsSchemaField =
269
- (optionSchema && optionSchema.type !== 'null' && (
270
- <SchemaFieldComponent {...this.props} schema={optionSchema} uiSchema={optionUiSchema} />
271
- )) ||
272
- null;
203
+ const translateEnum: TranslatableString = title
204
+ ? TranslatableString.TitleOptionPrefix
205
+ : TranslatableString.OptionPrefix;
206
+ const translateParams = title ? [title] : [];
207
+ const enumOptions = retrievedOptions.map((opt: { title?: string }, index: number) => {
208
+ const { title: uiTitle = opt.title } = getUiOptions<T, S, F>(optionsUiSchema[index]);
209
+ return {
210
+ label: uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))),
211
+ value: index,
212
+ };
213
+ });
273
214
 
274
- return (
275
- <MultiSchemaFieldTemplate
276
- schema={schema}
215
+ const selector =
216
+ !isOptionalRender || hasFormData ? (
217
+ <Widget
218
+ id={fieldId}
219
+ name={`${name}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
220
+ schema={{ type: 'number', default: 0 } as S}
221
+ onChange={onOptionChange}
222
+ onBlur={onBlur}
223
+ onFocus={onFocus}
224
+ disabled={disabled || isEmpty(enumOptions)}
225
+ multiple={false}
226
+ rawErrors={rawErrors}
227
+ errorSchema={fieldErrorSchema}
228
+ value={selectedOption >= 0 ? selectedOption : undefined}
229
+ options={{ enumOptions, ...uiOptions }}
277
230
  registry={registry}
278
- uiSchema={uiSchema}
279
- selector={selector}
280
- optionSchemaField={optionsSchemaField}
231
+ placeholder={placeholder}
232
+ autocomplete={autocomplete}
233
+ autofocus={autofocus}
234
+ label={title ?? name}
235
+ hideLabel={!displayLabel}
236
+ readonly={readonly}
281
237
  />
282
- );
283
- }
238
+ ) : undefined;
239
+
240
+ const optionsSchemaField =
241
+ (optionSchema && optionSchema.type !== 'null' && (
242
+ <SchemaFieldComponent {...props} schema={optionSchema} uiSchema={optionUiSchema} />
243
+ )) ||
244
+ null;
245
+
246
+ return (
247
+ <MultiSchemaFieldTemplate
248
+ schema={schema}
249
+ registry={registry}
250
+ uiSchema={uiSchema}
251
+ selector={selector}
252
+ optionSchemaField={optionsSchemaField}
253
+ />
254
+ );
284
255
  }
285
256
 
286
257
  export default AnyOfField;
@@ -7,18 +7,10 @@ import type {
7
7
  RJSFSchema,
8
8
  StrictRJSFSchema,
9
9
  } from '@rjsf/utils';
10
- import { asNumber } from '@rjsf/utils';
10
+ import { asNumber, getDecimalSeparator, getUiOptions, optionsList } from '@rjsf/utils';
11
11
 
12
- // Matches a string that ends in a . character, optionally followed by a sequence of
13
- // digits followed by any number of 0 characters up until the end of the line.
14
- // Ensuring that there is at least one prefixed character is important so that
15
- // you don't incorrectly match against "0".
12
+ // Static matchers for standard '.' separator used during normalization inside handleChange
16
13
  const trailingCharMatcherWithPrefix = /\.([0-9]*0)*$/;
17
-
18
- // This is used for trimming the trailing 0 and . characters without affecting
19
- // the rest of the string. Its possible to use one RegEx with groups for this
20
- // functionality, but it is fairly complex compared to simply defining two
21
- // different matchers.
22
14
  const trailingCharMatcher = /[0.]0*$/;
23
15
 
24
16
  /**
@@ -45,6 +37,9 @@ function NumberField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
45
37
  const [lastValue, setLastValue] = useState(initialValue);
46
38
  const { StringField } = registry.fields;
47
39
 
40
+ const separator = getDecimalSeparator();
41
+ const escapedSeparator = separator === '.' ? '\\.' : separator;
42
+
48
43
  let value = formData;
49
44
 
50
45
  /** Handle the change from the `StringField` to properly convert to a number
@@ -56,28 +51,31 @@ function NumberField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
56
51
  // Cache the original value in component state
57
52
  setLastValue(newValue);
58
53
 
54
+ // Convert locale separator to standard '.' first
55
+ const standardValue = typeof newValue === 'string' ? newValue.replace(separator, '.') : newValue;
56
+
59
57
  // Normalize decimals that don't start with a zero character in advance so
60
58
  // that the rest of the normalization logic is simpler
61
- const normalizedValue = `${newValue}`.startsWith('.') ? `0${newValue}` : newValue;
59
+ const normalizedValue = `${standardValue}`.startsWith('.') ? `0${standardValue}` : standardValue;
62
60
 
63
61
  // Check that the value is a string (this can happen if the widget used is a
64
62
  // <select>, due to an enum declaration etc) then, if the value ends in a
65
63
  // trailing decimal point or multiple zeroes, strip the trailing values
66
64
  const processed =
67
- typeof normalizedValue === 'string' && normalizedValue.match(trailingCharMatcherWithPrefix)
65
+ typeof normalizedValue === 'string' && trailingCharMatcherWithPrefix.exec(normalizedValue)
68
66
  ? asNumber(normalizedValue.replace(trailingCharMatcher, ''))
69
67
  : asNumber(normalizedValue);
70
68
 
71
69
  onChange(processed as unknown as T, path, errorSchema, id);
72
70
  },
73
- [onChange],
71
+ [onChange, separator],
74
72
  );
75
73
 
76
74
  if (typeof lastValue === 'string' && typeof value === 'number') {
77
75
  // Construct a regular expression that checks for a string that consists
78
- // of the formData value suffixed with zero or one '.' characters and zero
76
+ // of the formData value suffixed with zero or one locale separator characters and zero
79
77
  // or more '0' characters
80
- const re = new RegExp(`^(${String(value).replace('.', '\\.')})?\\.?0*$`);
78
+ const re = new RegExp(`^(${String(value).replace('.', escapedSeparator)})?${escapedSeparator}?0*$`);
81
79
 
82
80
  // If the cached "lastValue" is a match, use that instead of the formData
83
81
  // value to prevent the input value from changing in the UI
@@ -86,7 +84,23 @@ function NumberField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
86
84
  }
87
85
  }
88
86
 
89
- return <StringField {...props} formData={value} onChange={handleChange} />;
87
+ // Format value to use the locale separator for rendering if it is a number
88
+ let displayValue: T | undefined = value;
89
+ if (typeof value === 'number' && separator !== '.') {
90
+ const { schema, uiSchema } = props;
91
+ const { schemaUtils } = registry;
92
+ const enumOptions = schemaUtils.isSelect(schema) ? optionsList(schema, uiSchema) : undefined;
93
+ const defaultWidget = enumOptions ? 'select' : 'text';
94
+ const { widget = defaultWidget } = getUiOptions(uiSchema);
95
+
96
+ // Do not convert the value to a locale-specific string for radio, select,
97
+ // or hidden widgets because option matching relies on the original numeric value.
98
+ if (widget !== 'radio' && widget !== 'select' && widget !== 'hidden') {
99
+ displayValue = String(value).replace('.', separator) as unknown as T;
100
+ }
101
+ }
102
+
103
+ return <StringField {...props} formData={displayValue} onChange={handleChange} />;
90
104
  }
91
105
 
92
106
  export default NumberField;