@rjsf/utils 6.4.1 → 6.5.0

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 (40) hide show
  1. package/dist/index.cjs +198 -84
  2. package/dist/index.cjs.map +4 -4
  3. package/dist/utils.esm.js +198 -84
  4. package/dist/utils.esm.js.map +4 -4
  5. package/dist/utils.umd.js +185 -80
  6. package/lib/enumOptionSelectedValue.d.ts +15 -0
  7. package/lib/enumOptionSelectedValue.js +28 -0
  8. package/lib/enumOptionSelectedValue.js.map +1 -0
  9. package/lib/enumOptionValueDecoder.d.ts +17 -0
  10. package/lib/enumOptionValueDecoder.js +53 -0
  11. package/lib/enumOptionValueDecoder.js.map +1 -0
  12. package/lib/enumOptionValueEncoder.d.ts +15 -0
  13. package/lib/enumOptionValueEncoder.js +27 -0
  14. package/lib/enumOptionValueEncoder.js.map +1 -0
  15. package/lib/getInputProps.js +9 -0
  16. package/lib/getInputProps.js.map +1 -1
  17. package/lib/getOptionValueFormat.d.ts +16 -0
  18. package/lib/getOptionValueFormat.js +17 -0
  19. package/lib/getOptionValueFormat.js.map +1 -0
  20. package/lib/index.d.ts +7 -2
  21. package/lib/index.js +7 -2
  22. package/lib/index.js.map +1 -1
  23. package/lib/removeOptionalEmptyObjects.d.ts +17 -0
  24. package/lib/removeOptionalEmptyObjects.js +108 -0
  25. package/lib/removeOptionalEmptyObjects.js.map +1 -0
  26. package/lib/resolveUiSchema.d.ts +5 -19
  27. package/lib/resolveUiSchema.js +49 -95
  28. package/lib/resolveUiSchema.js.map +1 -1
  29. package/lib/tsconfig.tsbuildinfo +1 -1
  30. package/lib/types.d.ts +27 -1
  31. package/package.json +4 -4
  32. package/src/enumOptionSelectedValue.ts +39 -0
  33. package/src/enumOptionValueDecoder.ts +64 -0
  34. package/src/enumOptionValueEncoder.ts +33 -0
  35. package/src/getInputProps.ts +10 -0
  36. package/src/getOptionValueFormat.ts +17 -0
  37. package/src/index.ts +11 -2
  38. package/src/removeOptionalEmptyObjects.ts +127 -0
  39. package/src/resolveUiSchema.ts +55 -122
  40. package/src/types.ts +28 -1
package/lib/types.d.ts CHANGED
@@ -20,6 +20,13 @@ export type FormContextType = GenericObjectType;
20
20
  /** The interface for the test ID proxy objects that are returned by the `getTestId` utility function.
21
21
  */
22
22
  export type TestIdShape = Record<string, string>;
23
+ /** Controls how enum-backed widgets encode option values in their DOM `value` attributes.
24
+ *
25
+ * - `'indexed'`: options are encoded as their array index (default, historical behavior).
26
+ * - `'realValue'`: primitive option values are stringified directly, enabling native form
27
+ * submission. Object/array values still fall back to their index.
28
+ */
29
+ export type OptionValueFormat = 'indexed' | 'realValue';
23
30
  /** Function to generate HTML name attributes from path segments */
24
31
  export type NameGeneratorFunction = (path: FieldPathList, idPrefix: string, isMultiValue?: boolean) => string;
25
32
  /** Experimental feature that specifies the Array `minItems` default form state behavior
@@ -125,11 +132,15 @@ export type RangeSpecType = {
125
132
  max?: number;
126
133
  };
127
134
  /** Properties describing a Range specification in terms of attribute that can be added to the `HTML` `<input>` */
128
- export type InputPropsType = Omit<RangeSpecType, 'step'> & {
135
+ export type InputPropsType = {
129
136
  /** Specifies the type of the <input> element */
130
137
  type: string;
131
138
  /** Specifies the interval between legal numbers in an input field or "any" */
132
139
  step?: number | 'any';
140
+ /** Specifies a minimum value for an <input> element; accepts a number for numeric inputs or a string for date/time inputs */
141
+ min?: number | string;
142
+ /** Specifies the maximum value for an <input> element; accepts a number for numeric inputs or a string for date/time inputs */
143
+ max?: number | string;
133
144
  /** Specifies the `autoComplete` value for an <input> element */
134
145
  autoComplete?: HTMLInputElement['autocomplete'];
135
146
  /** Specifies a filter for what file types the user can upload. */
@@ -357,6 +368,21 @@ export type GlobalUISchemaOptions = GenericObjectType & {
357
368
  * both. To disable the Optional Data Field UI for a specific field, provide an empty array within the UI schema.
358
369
  */
359
370
  enableOptionalDataFieldForType?: ('object' | 'array')[];
371
+ /** Controls how enum-backed widgets (select, radio, checkboxes) encode option values
372
+ * in their DOM `value` attributes.
373
+ *
374
+ * - `'indexed'` (default): options are encoded as their array index. This is the
375
+ * historical behavior and keeps object/array enum values addressable without
376
+ * stringifying them.
377
+ * - `'realValue'`: primitive option values are stringified directly (e.g. `"foo"`,
378
+ * `"42"`, `"true"`). This enables native form submission and browser autocomplete
379
+ * since the submitted value matches the enum value. Object/array values still
380
+ * fall back to their index since `String(obj)` would produce `"[object Object]"`.
381
+ *
382
+ * The form data passed to `onChange` is always the typed enum value; this option
383
+ * only affects the DOM-level encoding.
384
+ */
385
+ optionValueFormat?: OptionValueFormat;
360
386
  };
361
387
  /** The set of options from the `Form` that will be available on the `Registry` for use in everywhere the `registry` is
362
388
  * available.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rjsf/utils",
3
- "version": "6.4.1",
3
+ "version": "6.5.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -65,11 +65,11 @@
65
65
  "react": ">=18"
66
66
  },
67
67
  "dependencies": {
68
- "@x0k/json-schema-merge": "^1.0.2",
68
+ "@x0k/json-schema-merge": "^1.0.3",
69
69
  "fast-uri": "^3.1.0",
70
70
  "jsonpointer": "^5.0.1",
71
- "lodash": "^4.17.23",
72
- "lodash-es": "^4.17.23",
71
+ "lodash": "^4.18.1",
72
+ "lodash-es": "^4.18.1",
73
73
  "react-is": "^18.3.1"
74
74
  },
75
75
  "devDependencies": {
@@ -0,0 +1,39 @@
1
+ import { EnumOptionsType, OptionValueFormat, StrictRJSFSchema, RJSFSchema } from './types';
2
+ import enumOptionsIndexForValue from './enumOptionsIndexForValue';
3
+
4
+ /** Computes the value to pass to a select element's `value` attribute.
5
+ *
6
+ * When `format` is `'realValue'`, converts form data values to strings.
7
+ * When `format` is `'indexed'` (the default), resolves to index-based values via
8
+ * `enumOptionsIndexForValue`. Returns `emptyValue` when the current value is empty.
9
+ *
10
+ * @param value - The current form data value
11
+ * @param enumOptions - The available enum options
12
+ * @param multiple - Whether the select allows multiple selections
13
+ * @param [format='indexed'] - How option values are encoded on the DOM
14
+ * @param emptyValue - The value to return when the selection is empty
15
+ * @returns The value to use for the select element's `value` attribute
16
+ */
17
+ export default function enumOptionSelectedValue<S extends StrictRJSFSchema = RJSFSchema>(
18
+ value: any,
19
+ enumOptions: EnumOptionsType<S>[] | undefined,
20
+ multiple: boolean,
21
+ format: OptionValueFormat = 'indexed',
22
+ emptyValue?: any,
23
+ ): any {
24
+ const isEmpty =
25
+ typeof value === 'undefined' ||
26
+ (multiple && Array.isArray(value) && value.length < 1) ||
27
+ (!multiple && value === emptyValue);
28
+
29
+ if (isEmpty) {
30
+ return emptyValue;
31
+ }
32
+
33
+ if (format === 'realValue') {
34
+ return multiple ? value.map(String) : String(value);
35
+ }
36
+
37
+ const indexes = enumOptionsIndexForValue<S>(value, enumOptions, multiple);
38
+ return typeof indexes === 'undefined' ? emptyValue : indexes;
39
+ }
@@ -0,0 +1,64 @@
1
+ import { EnumOptionsType, OptionValueFormat, StrictRJSFSchema, RJSFSchema } from './types';
2
+ import enumOptionsValueForIndex from './enumOptionsValueForIndex';
3
+
4
+ /** Resolves a single DOM value string back to its typed enum value in `'realValue'` mode.
5
+ *
6
+ * First attempts a reverse lookup by matching `String(opt.value)` against the input.
7
+ * If no option matches and the input parses as a valid index, falls back to the
8
+ * option at that index — this is how object/array enum values round-trip, since
9
+ * they are encoded as indices by the encoder.
10
+ *
11
+ * @param value - A single string value from a DOM attribute
12
+ * @param enumOptions - The available enum options
13
+ * @param emptyValue - The value to return when the input is empty, options are missing, or no match is found
14
+ * @returns The original typed enum value, or `emptyValue`
15
+ */
16
+ function decodeSingle<S extends StrictRJSFSchema = RJSFSchema>(
17
+ value: string,
18
+ enumOptions: EnumOptionsType<S>[] | undefined,
19
+ emptyValue?: unknown,
20
+ ): unknown {
21
+ if (value === '' || !Array.isArray(enumOptions)) {
22
+ return emptyValue;
23
+ }
24
+ const match = enumOptions.find((opt) => String(opt.value) === value);
25
+ if (match) {
26
+ return match.value;
27
+ }
28
+ // Fallback: value might be an index (for object/array enum values)
29
+ const index = Number(value);
30
+ if (!isNaN(index) && index >= 0 && index < enumOptions.length) {
31
+ return enumOptions[index].value;
32
+ }
33
+ return emptyValue;
34
+ }
35
+
36
+ /** Decodes a string from a DOM value attribute back to a typed enum value.
37
+ *
38
+ * When `format` is `'realValue'`, does a reverse lookup: finds the enum option
39
+ * whose `String(value)` matches the input string and returns the original typed value.
40
+ * For object/array values that were encoded as indices, falls back to index resolution.
41
+ *
42
+ * When `format` is `'indexed'` (the default), uses index-based resolution via
43
+ * `enumOptionsValueForIndex`.
44
+ *
45
+ * @param value - The string value(s) from the DOM
46
+ * @param enumOptions - The available enum options
47
+ * @param [format='indexed'] - How the values were encoded on the DOM
48
+ * @param emptyValue - The value to return for empty/missing selections
49
+ * @returns The original typed enum value(s)
50
+ */
51
+ export default function enumOptionValueDecoder<S extends StrictRJSFSchema = RJSFSchema>(
52
+ value: string | string[],
53
+ enumOptions: EnumOptionsType<S>[] | undefined,
54
+ format: OptionValueFormat = 'indexed',
55
+ emptyValue?: unknown,
56
+ ): unknown {
57
+ if (format !== 'realValue') {
58
+ return enumOptionsValueForIndex<S>(value, enumOptions, emptyValue);
59
+ }
60
+ if (Array.isArray(value)) {
61
+ return value.map((v) => decodeSingle(v, enumOptions, emptyValue));
62
+ }
63
+ return decodeSingle(value, enumOptions, emptyValue);
64
+ }
@@ -0,0 +1,33 @@
1
+ import isNil from 'lodash/isNil';
2
+
3
+ import { OptionValueFormat } from './types';
4
+
5
+ /** Encodes an enum option value into a string for a DOM value attribute.
6
+ *
7
+ * When `format` is `'realValue'`, primitive values are converted via `String()`.
8
+ * Non-primitive values (objects, arrays) fall back to the index since
9
+ * `String()` would produce `"[object Object]"`.
10
+ *
11
+ * When `format` is `'indexed'` (the default), returns the index as a string.
12
+ *
13
+ * @param value - The typed enum value
14
+ * @param index - The option's position in the enumOptions array
15
+ * @param [format='indexed'] - How to encode the value for the DOM attribute
16
+ * @returns The string to use as the DOM value attribute
17
+ */
18
+ export default function enumOptionValueEncoder(
19
+ value: unknown,
20
+ index: number,
21
+ format: OptionValueFormat = 'indexed',
22
+ ): string {
23
+ if (format !== 'realValue') {
24
+ return String(index);
25
+ }
26
+ if (isNil(value)) {
27
+ return '';
28
+ }
29
+ if (typeof value === 'object') {
30
+ return String(index);
31
+ }
32
+ return String(value);
33
+ }
@@ -47,6 +47,16 @@ export default function getInputProps<
47
47
  }
48
48
  }
49
49
 
50
+ // For date/time input types, propagate formatMinimum/formatMaximum to min/max
51
+ if (['date', 'datetime-local', 'time', 'week', 'month'].includes(inputProps.type)) {
52
+ if (schema.formatMinimum !== undefined) {
53
+ inputProps.min = schema.formatMinimum as string;
54
+ }
55
+ if (schema.formatMaximum !== undefined) {
56
+ inputProps.max = schema.formatMaximum as string;
57
+ }
58
+ }
59
+
50
60
  if (options.autocomplete) {
51
61
  inputProps.autoComplete = options.autocomplete;
52
62
  }
@@ -0,0 +1,17 @@
1
+ import { OptionValueFormat } from './types';
2
+
3
+ /** Resolves the effective `optionValueFormat` for enum-backed widgets.
4
+ *
5
+ * Provides a single source of truth for the default DOM encoding format
6
+ * (`'indexed'`) used by `SelectWidget`, `RadioWidget`, and `CheckboxesWidget`.
7
+ * Widgets should call this helper once and pass the result to
8
+ * `enumOptionValueEncoder`, `enumOptionValueDecoder`, and `enumOptionSelectedValue`
9
+ * rather than reading `options.optionValueFormat` directly.
10
+ *
11
+ * @param options - The widget options (typically from the `options` prop, already
12
+ * resolved from `ui:options` and `ui:globalOptions`)
13
+ * @returns The resolved `OptionValueFormat`, defaulting to `'indexed'` when not set
14
+ */
15
+ export default function getOptionValueFormat(options?: { optionValueFormat?: OptionValueFormat }): OptionValueFormat {
16
+ return options?.optionValueFormat ?? 'indexed';
17
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,9 @@ import dateRangeOptions from './dateRangeOptions';
8
8
  import deepEquals from './deepEquals';
9
9
  import shallowEquals from './shallowEquals';
10
10
  import englishStringTranslator from './englishStringTranslator';
11
+ import enumOptionSelectedValue from './enumOptionSelectedValue';
12
+ import enumOptionValueDecoder from './enumOptionValueDecoder';
13
+ import enumOptionValueEncoder from './enumOptionValueEncoder';
11
14
  import enumOptionsDeselectValue from './enumOptionsDeselectValue';
12
15
  import enumOptionsIndexForValue from './enumOptionsIndexForValue';
13
16
  import enumOptionsIsSelected from './enumOptionsIsSelected';
@@ -20,6 +23,7 @@ import getDateElementProps, { DateElementFormat, DateElementProp } from './getDa
20
23
  import getDiscriminatorFieldFromSchema from './getDiscriminatorFieldFromSchema';
21
24
  import getInputProps from './getInputProps';
22
25
  import getOptionMatchingSimpleDiscriminator from './getOptionMatchingSimpleDiscriminator';
26
+ import getOptionValueFormat from './getOptionValueFormat';
23
27
  import getSchemaType from './getSchemaType';
24
28
  import getSubmitButtonOptions from './getSubmitButtonOptions';
25
29
  import getTemplate from './getTemplate';
@@ -53,12 +57,13 @@ import mergeDefaultsWithFormData from './mergeDefaultsWithFormData';
53
57
  import mergeObjects from './mergeObjects';
54
58
  import mergeSchemas from './mergeSchemas';
55
59
  import optionsList from './optionsList';
60
+ import removeOptionalEmptyObjects from './removeOptionalEmptyObjects';
56
61
  import orderProperties from './orderProperties';
57
62
  import pad from './pad';
58
63
  import parseDateString from './parseDateString';
59
64
  import rangeSpec from './rangeSpec';
60
65
  import replaceStringParameters from './replaceStringParameters';
61
- import resolveUiSchema, { expandUiSchemaDefinitions } from './resolveUiSchema';
66
+ import resolveUiSchema from './resolveUiSchema';
62
67
  import schemaRequiresTrueValue from './schemaRequiresTrueValue';
63
68
  import shouldRender, { ComponentUpdateStrategy } from './shouldRender';
64
69
  import shouldRenderOptionalField from './shouldRenderOptionalField';
@@ -107,6 +112,9 @@ export {
107
112
  deepEquals,
108
113
  descriptionId,
109
114
  englishStringTranslator,
115
+ enumOptionSelectedValue,
116
+ enumOptionValueDecoder,
117
+ enumOptionValueEncoder,
110
118
  enumOptionsDeselectValue,
111
119
  enumOptionsIndexForValue,
112
120
  enumOptionsIsSelected,
@@ -121,6 +129,7 @@ export {
121
129
  getDiscriminatorFieldFromSchema,
122
130
  getInputProps,
123
131
  getOptionMatchingSimpleDiscriminator,
132
+ getOptionValueFormat,
124
133
  getSchemaType,
125
134
  getSubmitButtonOptions,
126
135
  getTemplate,
@@ -152,9 +161,9 @@ export {
152
161
  pad,
153
162
  parseDateString,
154
163
  rangeSpec,
164
+ removeOptionalEmptyObjects,
155
165
  replaceStringParameters,
156
166
  resolveUiSchema,
157
- expandUiSchemaDefinitions,
158
167
  schemaRequiresTrueValue,
159
168
  shallowEquals,
160
169
  shouldRender,
@@ -0,0 +1,127 @@
1
+ import isNil from 'lodash/isNil';
2
+
3
+ import isObject from './isObject';
4
+ import { FormContextType, GenericObjectType, RJSFSchema, StrictRJSFSchema, ValidatorType } from './types';
5
+ import retrieveSchema from './schema/retrieveSchema';
6
+
7
+ /** Determines whether a value is considered "empty" for the purposes of optional object pruning.
8
+ * A value is empty if it is `undefined`, `null`, an empty string, or an object where all own
9
+ * properties are themselves empty.
10
+ *
11
+ * @param value - The value to check
12
+ * @returns True if the value is considered empty
13
+ */
14
+ function isValueEmpty(value: unknown): boolean {
15
+ if (isNil(value) || value === '') {
16
+ return true;
17
+ }
18
+ if (Array.isArray(value)) {
19
+ // An empty array is considered empty; a non-empty array is not
20
+ return value.length === 0;
21
+ }
22
+ if (isObject(value)) {
23
+ const obj = value as GenericObjectType;
24
+ const keys = Object.keys(obj);
25
+ return keys.every((key) => isValueEmpty(obj[key]));
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /** Recursively removes optional objects from the `formData` that are empty (i.e., all their fields
31
+ * are undefined, null, empty strings, or themselves empty optional objects). This solves the problem
32
+ * where interacting with fields inside an optional object "activates" it permanently, making the
33
+ * form unsubmittable when the optional object has required inner fields.
34
+ *
35
+ * An object property is considered "optional" when it is NOT listed in its parent schema's `required`
36
+ * array.
37
+ *
38
+ * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
39
+ * @param schema - The JSON schema describing the `formData`
40
+ * @param [rootSchema] - The root schema, used primarily to look up `$ref`s
41
+ * @param [formData] - The current form data to prune
42
+ * @returns - A new copy of `formData` with empty optional objects removed, or `undefined` if the
43
+ * entire formData was pruned
44
+ */
45
+ export default function removeOptionalEmptyObjects<
46
+ T = any,
47
+ S extends StrictRJSFSchema = RJSFSchema,
48
+ F extends FormContextType = any,
49
+ >(validator: ValidatorType<T, S, F>, schema: S, rootSchema?: S, formData?: T): T | undefined {
50
+ if (!isObject(schema)) {
51
+ return formData;
52
+ }
53
+
54
+ const resolvedSchema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
55
+
56
+ if (Array.isArray(formData)) {
57
+ const itemsSchema = resolvedSchema.items as S | S[];
58
+ if (!itemsSchema) {
59
+ return formData;
60
+ }
61
+
62
+ let hasChanges = false;
63
+ const mapped = formData.map((item, index) => {
64
+ let itemSchema = itemsSchema as S;
65
+ if (Array.isArray(itemsSchema)) {
66
+ itemSchema = itemsSchema[index] || (resolvedSchema.additionalItems as S) || ({} as S);
67
+ }
68
+ const cleaned = removeOptionalEmptyObjects<T, S, F>(validator, itemSchema, rootSchema, item);
69
+ if (cleaned !== item) {
70
+ hasChanges = true;
71
+ }
72
+ return cleaned === undefined ? ({} as T) : cleaned;
73
+ });
74
+
75
+ // Although T is an array type here, we still need to cast it back to T since TS
76
+ // doesn't narrow the generic T automatically
77
+ return hasChanges ? (mapped as T) : formData;
78
+ }
79
+
80
+ const { properties, required: requiredFields = [] } = resolvedSchema;
81
+
82
+ if (!isObject(formData) || !properties) {
83
+ return formData;
84
+ }
85
+
86
+ const result: GenericObjectType = {};
87
+ const data = formData as GenericObjectType;
88
+ let hasAnyValue = false;
89
+
90
+ for (const key of Object.keys(data)) {
91
+ const value = data[key];
92
+ const propertySchema = (properties[key] || {}) as S;
93
+ const isRequired = (requiredFields as string[]).includes(key);
94
+
95
+ const isObj = isObject(value);
96
+ const isArr = Array.isArray(value);
97
+
98
+ if ((isObj || isArr) && properties[key]) {
99
+ // Recursively process nested objects and arrays
100
+ const cleaned = removeOptionalEmptyObjects<T, S, F>(validator, propertySchema, rootSchema, value as T);
101
+
102
+ if (!isRequired && isValueEmpty(cleaned)) {
103
+ // This is an optional property and the cleaned result is empty — omit it
104
+ continue;
105
+ }
106
+
107
+ result[key] = cleaned;
108
+ hasAnyValue = true;
109
+ } else if (!isRequired && isValueEmpty(value) && properties[key]) {
110
+ // Optional scalar property that is empty — omit it
111
+ continue;
112
+ } else {
113
+ result[key] = value;
114
+ if (!isValueEmpty(value)) {
115
+ hasAnyValue = true;
116
+ }
117
+ }
118
+ }
119
+
120
+ // If the entire object ended up empty after pruning, return undefined so that the
121
+ // caller (which may itself be a recursive call) can decide whether to keep or drop it
122
+ if (!hasAnyValue && Object.keys(result).length === 0) {
123
+ return undefined;
124
+ }
125
+
126
+ return result as T;
127
+ }
@@ -1,112 +1,81 @@
1
- import {
2
- ADDITIONAL_PROPERTIES_KEY,
3
- ALL_OF_KEY,
4
- ANY_OF_KEY,
5
- ITEMS_KEY,
6
- ONE_OF_KEY,
7
- PROPERTIES_KEY,
8
- REF_KEY,
9
- RJSF_REF_KEY,
10
- } from './constants';
1
+ import isEmpty from 'lodash/isEmpty';
2
+
3
+ import { ANY_OF_KEY, ONE_OF_KEY, REF_KEY, RJSF_REF_KEY } from './constants';
11
4
  import findSchemaDefinition from './findSchemaDefinition';
12
- import isObject from './isObject';
13
5
  import mergeObjects from './mergeObjects';
14
6
  import { FormContextType, GenericObjectType, Registry, RJSFSchema, StrictRJSFSchema, UiSchema } from './types';
15
7
 
16
- // Keywords where child schemas map to uiSchema at the SAME key
17
- const SAME_KEY_KEYWORDS = [ITEMS_KEY, ADDITIONAL_PROPERTIES_KEY] as const;
18
-
19
- // Keywords where child schemas are in an array, each mapping to uiSchema[keyword][i]
20
- const ARRAY_KEYWORDS = [ONE_OF_KEY, ANY_OF_KEY, ALL_OF_KEY] as const;
21
-
22
- /** Expands `ui:definitions` into the uiSchema by walking the schema tree and finding all `$ref`s.
23
- * Called once at form initialization to pre-expand definitions into the uiSchema structure.
8
+ /** Resolves the uiSchema for a given schema, considering `ui:definitions` stored in the registry.
24
9
  *
25
- * For recursive schemas, expansion stops at recursion points to avoid infinite loops.
26
- * Runtime resolution via `resolveUiSchema` handles these cases using registry definitions.
10
+ * Called at runtime for each field. When the schema contains a `$ref`, looks up the corresponding
11
+ * uiSchema definition from `registry.uiSchemaDefinitions` and merges it with local overrides.
12
+ * For schemas with `oneOf`/`anyOf` branches, also populates `uiSchema[keyword][i]` for branches
13
+ * whose `$ref` matches a definition, so `MultiSchemaField` can read dropdown option titles.
27
14
  *
28
- * @param currentSchema - The current schema node being processed
29
- * @param uiSchema - The uiSchema at the current path
30
- * @param registry - The registry containing rootSchema and uiSchemaDefinitions
31
- * @param visited - Set of $refs already visited (to detect recursion)
32
- * @returns - The expanded uiSchema with definitions merged in
15
+ * Resolution order (later sources override earlier):
16
+ * 1. `ui:definitions[$ref]` - base definition from registry
17
+ * 2. `localUiSchema` - local overrides at current path
18
+ *
19
+ * @param schema - The JSON schema (may contain `$ref` or `RJSF_REF_KEY`)
20
+ * @param localUiSchema - The uiSchema at the current path (local overrides)
21
+ * @param registry - The registry containing `uiSchemaDefinitions`
22
+ * @returns - The resolved uiSchema with definitions merged in
33
23
  */
34
- export function expandUiSchemaDefinitions<
24
+ export default function resolveUiSchema<
35
25
  T = any,
36
26
  S extends StrictRJSFSchema = RJSFSchema,
37
27
  F extends FormContextType = any,
38
- >(
39
- currentSchema: S,
40
- uiSchema: UiSchema<T, S, F>,
41
- registry: Registry<T, S, F>,
42
- visited: Set<string> = new Set(),
43
- ): UiSchema<T, S, F> {
44
- const { rootSchema, uiSchemaDefinitions: definitions } = registry;
45
- let result = { ...uiSchema };
46
- let resolvedSchema = currentSchema;
47
-
48
- const ref = currentSchema[REF_KEY] as string | undefined;
49
- const isRecursive = ref && visited.has(ref);
50
-
51
- if (ref) {
52
- visited.add(ref);
53
-
54
- if (definitions && ref in definitions) {
55
- result = mergeObjects(definitions[ref] as GenericObjectType, result as GenericObjectType) as UiSchema<T, S, F>;
56
- }
57
-
58
- if (isRecursive) {
59
- return result;
60
- }
28
+ >(schema: S, localUiSchema: UiSchema<T, S, F> | undefined, registry: Registry<T, S, F>): UiSchema<T, S, F> {
29
+ const ref = ((schema as GenericObjectType)[RJSF_REF_KEY] ?? schema[REF_KEY]) as string | undefined;
30
+ const definitions = registry.uiSchemaDefinitions;
31
+ const definitionUiSchema = ref && definitions ? definitions[ref] : undefined;
61
32
 
62
- try {
63
- resolvedSchema = findSchemaDefinition<S>(ref, rootSchema as S);
64
- } catch {
65
- resolvedSchema = currentSchema;
66
- }
33
+ let result: UiSchema<T, S, F>;
34
+ if (!definitionUiSchema) {
35
+ result = localUiSchema || {};
36
+ } else if (!localUiSchema || isEmpty(localUiSchema)) {
37
+ result = { ...definitionUiSchema };
38
+ } else {
39
+ result = mergeObjects(definitionUiSchema as GenericObjectType, localUiSchema as GenericObjectType) as UiSchema<
40
+ T,
41
+ S,
42
+ F
43
+ >;
67
44
  }
68
45
 
69
- // Process properties (each property maps to uiSchema[propName] - flattened)
70
- const properties = resolvedSchema[PROPERTIES_KEY];
71
- if (properties && isObject(properties)) {
72
- for (const [propName, propSchema] of Object.entries(properties as Record<string, S>)) {
73
- const propUiSchema = (result[propName] || {}) as UiSchema<T, S, F>;
74
- const expanded = expandUiSchemaDefinitions(propSchema, propUiSchema, registry, new Set(visited));
75
- if (Object.keys(expanded).length > 0) {
76
- result[propName] = expanded;
46
+ // Walk oneOf/anyOf branches to populate uiSchema[keyword][i] so MultiSchemaField
47
+ // can read dropdown option titles at the parent level.
48
+ if (definitions) {
49
+ let resolvedSchema: S = schema;
50
+ if (ref && schema[REF_KEY] && !(schema as GenericObjectType)[RJSF_REF_KEY]) {
51
+ try {
52
+ resolvedSchema = findSchemaDefinition<S>(ref, registry.rootSchema as S);
53
+ } catch (e) {
54
+ console.warn('could not resolve $ref in resolveUiSchema:\n', e);
55
+ return result;
77
56
  }
78
57
  }
79
- }
80
58
 
81
- // Process keywords where child maps to same key in uiSchema (items, additionalProperties)
82
- for (const keyword of SAME_KEY_KEYWORDS) {
83
- const subSchema = resolvedSchema[keyword];
84
- if (subSchema && isObject(subSchema) && !Array.isArray(subSchema)) {
85
- const currentUiSchema = result[keyword];
86
- if (typeof currentUiSchema !== 'function') {
87
- const subUiSchema = ((currentUiSchema as GenericObjectType) || {}) as UiSchema<T, S, F>;
88
- const expanded = expandUiSchemaDefinitions(subSchema as S, subUiSchema, registry, new Set(visited));
89
- if (Object.keys(expanded).length > 0) {
90
- (result as GenericObjectType)[keyword] = expanded;
91
- }
59
+ for (const keyword of [ONE_OF_KEY, ANY_OF_KEY] as const) {
60
+ const schemaOptions = resolvedSchema[keyword];
61
+ if (!Array.isArray(schemaOptions) || schemaOptions.length === 0) {
62
+ continue;
92
63
  }
93
- }
94
- }
95
64
 
96
- // Process array keywords (oneOf, anyOf, allOf) - each option maps to uiSchema[keyword][i]
97
- for (const keyword of ARRAY_KEYWORDS) {
98
- const schemaOptions = resolvedSchema[keyword];
99
- if (Array.isArray(schemaOptions) && schemaOptions.length > 0) {
100
65
  const currentUiSchemaArray = (result as GenericObjectType)[keyword];
101
66
  const uiSchemaArray: UiSchema<T, S, F>[] = Array.isArray(currentUiSchemaArray) ? [...currentUiSchemaArray] : [];
102
67
 
103
68
  let hasExpanded = false;
104
69
  for (let i = 0; i < schemaOptions.length; i++) {
105
- const optionSchema = schemaOptions[i] as S;
106
- const optionUiSchema = (uiSchemaArray[i] || {}) as UiSchema<T, S, F>;
107
- const expanded = expandUiSchemaDefinitions(optionSchema, optionUiSchema, registry, new Set(visited));
108
- if (Object.keys(expanded).length > 0) {
109
- uiSchemaArray[i] = expanded;
70
+ const option = schemaOptions[i] as GenericObjectType | undefined;
71
+ const optionRef = (option?.[RJSF_REF_KEY] ?? option?.[REF_KEY]) as string | undefined;
72
+ if (optionRef && optionRef in definitions) {
73
+ const optionUiSchema = (uiSchemaArray[i] || {}) as GenericObjectType;
74
+ uiSchemaArray[i] = mergeObjects(definitions[optionRef] as GenericObjectType, optionUiSchema) as UiSchema<
75
+ T,
76
+ S,
77
+ F
78
+ >;
110
79
  hasExpanded = true;
111
80
  }
112
81
  }
@@ -119,39 +88,3 @@ export function expandUiSchemaDefinitions<
119
88
 
120
89
  return result;
121
90
  }
122
-
123
- /** Resolves the uiSchema for a given schema, considering `ui:definitions` stored in the registry.
124
- *
125
- * This function is called at runtime for each field. It handles recursive schemas where the
126
- * pre-expansion in `expandUiSchemaDefinitions` couldn't go deeper.
127
- *
128
- * When the schema contains a `$ref`, this function looks up the corresponding uiSchema definition
129
- * from `registry.uiSchemaDefinitions` and merges it with any local uiSchema overrides.
130
- *
131
- * Resolution order (later sources override earlier):
132
- * 1. `ui:definitions[$ref]` - base definition from registry
133
- * 2. `localUiSchema` - local overrides at current path
134
- *
135
- * @param schema - The JSON schema (may still contain `$ref` for recursive schemas)
136
- * @param localUiSchema - The uiSchema at the current path (local overrides)
137
- * @param registry - The registry containing `uiSchemaDefinitions`
138
- * @returns - The resolved uiSchema with definitions merged in
139
- */
140
- export default function resolveUiSchema<
141
- T = any,
142
- S extends StrictRJSFSchema = RJSFSchema,
143
- F extends FormContextType = any,
144
- >(schema: S, localUiSchema: UiSchema<T, S, F> | undefined, registry: Registry<T, S, F>): UiSchema<T, S, F> {
145
- const ref = ((schema as GenericObjectType)[RJSF_REF_KEY] ?? schema[REF_KEY]) as string | undefined;
146
- const definitionUiSchema = ref ? registry.uiSchemaDefinitions?.[ref] : undefined;
147
-
148
- if (!definitionUiSchema) {
149
- return localUiSchema || {};
150
- }
151
-
152
- if (!localUiSchema || Object.keys(localUiSchema).length === 0) {
153
- return { ...definitionUiSchema };
154
- }
155
-
156
- return mergeObjects(definitionUiSchema as GenericObjectType, localUiSchema as GenericObjectType) as UiSchema<T, S, F>;
157
- }