Skip to content

Commit e71cb8d

Browse files
Adding feature to support const as default bug fix seeming like a regression (#4381)
Fixes #4344, #4361 and #4377 - In `@rjsf/utils`: - Updated the `Experimental_DefaultFormStateBehavior` type to add new optional `constAsDefaults` prop with three choices - Updated `getDefaultFormState()` to respond to the new `constAsDefaults` feature to limit `const` as defaults based on the `never` or `skipOneOf` choices - Added tests for `getDefaultFormState()` to verify the new feature - In `@rjsf/core`: - Updated `SchemaField` to remove making the field readonly when const - In `playground`: - Updated `Header` to add support for `constAsDefaults` - Updated the `CHANGELOG.md` accordingly
1 parent 1e12bfe commit e71cb8d

File tree

7 files changed

+305
-10
lines changed

7 files changed

+305
-10
lines changed

CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19+
# 5.23.0
20+
21+
## @rjsf/core
22+
23+
- Updated `SchemaField` to no longer make schema fields with const read-only by default, partially fixing [#4344](https://github.com/rjsf-team/react-jsonschema-form/issues/4344)
24+
25+
## @rjsf/utils
26+
27+
- Updated `Experimental_DefaultFormStateBehavior` to add a new `constAsDefaults` option
28+
- Updated `getDefaultFormState()` to use the new `constAsDefaults` option to control how const is used for defaulting, fixing [#4344](https://github.com/rjsf-team/react-jsonschema-form/issues/4344), [#4361](https://github.com/rjsf-team/react-jsonschema-form/issues/4361) and [#4377](https://github.com/rjsf-team/react-jsonschema-form/issues/4377)
29+
30+
## Dev / docs / playground
31+
32+
- Updated the playground to add a selector for the `constAsDefaults` option
33+
1934
# 5.22.4
2035

2136
## @rjsf/utils

packages/core/src/components/fields/SchemaField.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
151151

152152
const FieldComponent = getFieldComponent<T, S, F>(schema, uiOptions, idSchema, registry);
153153
const disabled = Boolean(uiOptions.disabled ?? props.disabled);
154-
const readonly = Boolean(
155-
uiOptions.readonly ?? (props.readonly || props.schema.const || props.schema.readOnly || schema.readOnly)
156-
);
154+
const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly));
157155
const uiSchemaHideError = uiOptions.hideError;
158156
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
159157
const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError);

packages/docs/docs/api-reference/form-props.md

+12
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,18 @@ render(
251251
);
252252
```
253253

254+
### constAsDefaults
255+
256+
Optional enumerated flag controlling how const values are merged into the form data as defaults when dealing with undefined values, defaulting to `always`.
257+
The defaulting behavior for this flag will always be controlled by the `emptyObjectField` flag value.
258+
For instance, if `populateRequiredDefaults` is set and the const value is not required, it will not be set.
259+
260+
| Flag Value | Description |
261+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
262+
| `always` | A const value will always be merged into the form as a default. If there is are const values in a `oneOf` (for instance to create an enumeration with title different from the values), the first const value will be defaulted |
263+
| `skipOneOf` | If const is in a `oneOf` it will NOT pick the first value as a default |
264+
| `never` | A const value will never be used as a default |
265+
254266
### mergeDefaultsIntoFormData
255267

256268
Optional enumerated flag controlling how the defaults are merged into the form data when dealing with undefined values, defaulting to `useFormDataIfPresent`.

packages/playground/src/components/Header.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,28 @@ const liveSettingsSelectSchema: RJSFSchema = {
128128
},
129129
],
130130
},
131+
constAsDefaults: {
132+
type: 'string',
133+
title: 'const as default behavior',
134+
default: 'always',
135+
oneOf: [
136+
{
137+
type: 'string',
138+
title: 'A const value will always be merged into the form as a default',
139+
enum: ['always'],
140+
},
141+
{
142+
type: 'string',
143+
title: 'If const is in a `oneOf` it will NOT pick the first value as a default',
144+
enum: ['skipOneOf'],
145+
},
146+
{
147+
type: 'string',
148+
title: 'A const value will never be used as a default',
149+
enum: ['never'],
150+
},
151+
],
152+
},
131153
emptyObjectFields: {
132154
type: 'string',
133155
title: 'Object fields default behavior',

packages/utils/src/schema/getDefaultFormState.ts

+23-7
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ import {
3131
} from '../types';
3232
import isMultiSelect from './isMultiSelect';
3333
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
34-
import isConstant from '../isConstant';
3534
import { JSONSchema7Object } from 'json-schema';
3635

36+
const PRIMITIVE_TYPES = ['string', 'number', 'integer', 'boolean', 'null'];
37+
3738
/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
3839
*/
3940
export enum AdditionalItemsHandling {
@@ -199,9 +200,10 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
199200
let defaults: T | T[] | undefined = parentDefaults;
200201
// If we get a new schema, then we need to recompute defaults again for the new schema found.
201202
let schemaToCompute: S | null = null;
203+
let experimental_dfsb_to_compute = experimental_defaultFormStateBehavior;
202204
let updatedRecurseList = _recurseList;
203205

204-
if (isConstant(schema)) {
206+
if (schema[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults !== 'never') {
205207
defaults = schema.const as unknown as T;
206208
} else if (isObject(defaults) && isObject(schema.default)) {
207209
// For object defaults, only override parent defaults that are defined in
@@ -250,6 +252,15 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
250252
return undefined;
251253
}
252254
const discriminator = getDiscriminatorFieldFromSchema<S>(schema);
255+
const { type = 'null' } = remaining;
256+
if (
257+
!Array.isArray(type) &&
258+
PRIMITIVE_TYPES.includes(type) &&
259+
experimental_dfsb_to_compute?.constAsDefaults === 'skipOneOf'
260+
) {
261+
// If we are in a oneOf of a primitive type, then we want to pass constAsDefaults as 'never' for the recursion
262+
experimental_dfsb_to_compute = { ...experimental_dfsb_to_compute, constAsDefaults: 'never' };
263+
}
253264
schemaToCompute = oneOf![
254265
getClosestMatchingOption<T, S, F>(
255266
validator,
@@ -285,7 +296,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
285296
rootSchema,
286297
includeUndefinedValues,
287298
_recurseList: updatedRecurseList,
288-
experimental_defaultFormStateBehavior,
299+
experimental_defaultFormStateBehavior: experimental_dfsb_to_compute,
289300
parentDefaults: defaults as T | undefined,
290301
rawFormData: formData as T,
291302
required,
@@ -337,9 +348,12 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
337348
const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce(
338349
(acc: GenericObjectType, key: string) => {
339350
const propertySchema = get(retrievedSchema, [PROPERTIES_KEY, key]);
340-
// Check if the parent schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
351+
// Check if the parent schema has a const property defined AND we are supporting const as defaults, then we
352+
// should always return the computedDefault since it's coming from the const.
341353
const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined;
342-
const hasConst = (isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst;
354+
const hasConst =
355+
((isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst) &&
356+
experimental_defaultFormStateBehavior?.constAsDefaults !== 'never';
343357
// Compute the defaults for this node, with the parent defaults we might
344358
// have from a previous run: defaults[key].
345359
const computedDefault = computeDefaults<T, S, F>(validator, propertySchema, {
@@ -481,8 +495,10 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
481495
}
482496
}
483497

484-
// Check if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
485-
const hasConst = isObject(schema) && CONST_KEY in schema;
498+
// Check if the schema has a const property defined AND we are supporting const as defaults, then we should always
499+
// return the computedDefault since it's coming from the const.
500+
const hasConst =
501+
isObject(schema) && CONST_KEY in schema && experimental_defaultFormStateBehavior?.constAsDefaults !== 'never';
486502
if (hasConst === false) {
487503
if (neverPopulate) {
488504
return defaults ?? emptyDefault;

packages/utils/src/types.ts

+12
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ export type Experimental_DefaultFormStateBehavior = {
9696
* default value instead
9797
*/
9898
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
99+
/** Optional enumerated flag controlling how const values are merged into the form data as defaults when dealing with
100+
* undefined values, defaulting to `always`. The defaulting behavior for this flag will always be controlled by the
101+
* `emptyObjectField` flag value. For instance, if `populateRequiredDefaults` is set and the const value is not
102+
* required, it will not be set.
103+
* - `always`: A const value will always be merged into the form as a default. If there is are const values in a
104+
* `oneOf` (for instance to create an enumeration with title different from the values), the first const value
105+
* will be defaulted
106+
* - `skipOneOf`: If const is in a `oneOf` it will NOT pick the first value as a default
107+
* - `never`: A const value will never be used as a default
108+
*
109+
*/
110+
constAsDefaults?: 'always' | 'skipOneOf' | 'never';
99111
};
100112

101113
/** Optional function that allows for custom merging of `allOf` schemas

0 commit comments

Comments
 (0)