Skip to content

Make fields with const pre-fiiled and readonly #3843 #4326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 21, 2024
Merged
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 5.21.3
# 5.22.0

## @rjsf/utils

- Made fields with const property pre-filled and readonly, fixing [#2600](https://github.com/rjsf-team/react-jsonschema-form/issues/2600)
- Added `experimental_customMergeAllOf` option to `retrieveSchema` to allow custom merging of `allOf` schemas

# 5.21.2
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/components/fields/ObjectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,18 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
const newFormData = { ...formData } as T;

let type: RJSFSchema['type'] = undefined;
let constValue: RJSFSchema['const'] = undefined;
let defaultValue: RJSFSchema['default'] = undefined;
if (isObject(schema.additionalProperties)) {
type = schema.additionalProperties.type;
constValue = schema.additionalProperties.const;
defaultValue = schema.additionalProperties.default;
let apSchema = schema.additionalProperties;
if (REF_KEY in apSchema) {
const { schemaUtils } = registry;
apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[REF_KEY] } as S, formData);
type = apSchema.type;
constValue = apSchema.const;
defaultValue = apSchema.default;
}
if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) {
Expand All @@ -219,8 +222,9 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
}

const newKey = this.getAvailableKey('newKey', newFormData);
const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type);
// Cast this to make the `set` work properly
set(newFormData as GenericObjectType, newKey, defaultValue ?? this.getDefaultValue(type));
set(newFormData as GenericObjectType, newKey, newValue);

onChange(newFormData);
};
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/components/fields/SchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e

const FieldComponent = getFieldComponent<T, S, F>(schema, uiOptions, idSchema, registry);
const disabled = Boolean(uiOptions.disabled ?? props.disabled);
const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly));
const readonly = Boolean(
uiOptions.readonly ?? (props.readonly || props.schema.const || props.schema.readOnly || schema.readOnly)
);
const uiSchemaHideError = uiOptions.hideError;
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError);
Expand Down
44 changes: 32 additions & 12 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty';

import {
ANY_OF_KEY,
CONST_KEY,
DEFAULT_KEY,
DEPENDENCIES_KEY,
PROPERTIES_KEY,
Expand Down Expand Up @@ -30,6 +31,8 @@ import {
} from '../types';
import isMultiSelect from './isMultiSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
import isConstant from '../isConstant';
import { JSONSchema7Object } from 'json-schema';

/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
*/
Expand Down Expand Up @@ -93,6 +96,7 @@ export function getInnerSchemaForArrayItem<S extends StrictRJSFSchema = RJSFSche
* @param requiredFields - The list of fields that are required
* @param experimental_defaultFormStateBehavior - Optional configuration object, if provided, allows users to override
* default form state behavior
* @param isConst - Optional flag, if true, indicates that the schema has a const property defined, thus we should always return the computedDefault since it's coming from the const.
*/
function maybeAddDefaultToObject<T = any>(
obj: GenericObjectType,
Expand All @@ -101,10 +105,13 @@ function maybeAddDefaultToObject<T = any>(
includeUndefinedValues: boolean | 'excludeObjectChildren',
isParentRequired?: boolean,
requiredFields: string[] = [],
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {}
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {},
isConst = false
) {
const { emptyObjectFields = 'populateAllDefaults' } = experimental_defaultFormStateBehavior;
if (includeUndefinedValues) {
if (includeUndefinedValues || isConst) {
// If includeUndefinedValues
// Or if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
obj[key] = computedDefault;
} else if (emptyObjectFields !== 'skipDefaults') {
if (isObject(computedDefault)) {
Expand Down Expand Up @@ -194,7 +201,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
let schemaToCompute: S | null = null;
let updatedRecurseList = _recurseList;

if (isObject(defaults) && isObject(schema.default)) {
if (isConstant(schema)) {
defaults = schema.const as unknown as T;
} else if (isObject(defaults) && isObject(schema.default)) {
// For object defaults, only override parent defaults that are defined in
// schema.default.
defaults = mergeObjects(defaults!, schema.default as GenericObjectType) as T;
Expand Down Expand Up @@ -324,11 +333,16 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf)
: schema;
const parentConst = retrievedSchema[CONST_KEY];
const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce(
(acc: GenericObjectType, key: string) => {
const propertySchema = get(retrievedSchema, [PROPERTIES_KEY, key]);
// Check if the parent schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined;
const hasConst = (isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst;
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
const computedDefault = computeDefaults<T, S, F>(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), {
const computedDefault = computeDefaults<T, S, F>(validator, propertySchema, {
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
Expand All @@ -345,7 +359,8 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
includeUndefinedValues,
required,
retrievedSchema.required,
experimental_defaultFormStateBehavior
experimental_defaultFormStateBehavior,
hasConst
);
return acc;
},
Expand Down Expand Up @@ -458,13 +473,17 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
}
}

if (neverPopulate) {
return defaults ?? emptyDefault;
}
if (ignoreMinItemsFlagSet && !required) {
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
// return form data/defaults
return defaults ? defaults : undefined;
// Check if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
const hasConst = isObject(schema) && CONST_KEY in schema;
if (hasConst === false) {
if (neverPopulate) {
return defaults ?? emptyDefault;
}
if (ignoreMinItemsFlagSet && !required) {
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
// return form data/defaults
return defaults ? defaults : undefined;
}
}

const defaultsLength = Array.isArray(defaults) ? defaults.length : 0;
Expand Down Expand Up @@ -562,6 +581,7 @@ export default function getDefaultFormState<
experimental_customMergeAllOf,
rawFormData: formData,
});

if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
// No form data? Use schema defaults.
return defaults;
Expand Down
146 changes: 146 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,62 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
it('throws error when schema is not an object', () => {
expect(() => getDefaultFormState(testValidator, null as unknown as RJSFSchema)).toThrowError('Invalid schema:');
});
it('test an object const value merge with formData', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
localConst: {
type: 'string',
const: 'local',
},
RootConst: {
type: 'object',
properties: {
attr1: {
type: 'number',
},
attr2: {
type: 'boolean',
},
},
const: {
attr1: 1,
attr2: true,
},
},
RootAndLocalConst: {
type: 'string',
const: 'FromLocal',
},
fromFormData: {
type: 'string',
},
},
const: {
RootAndLocalConst: 'FromRoot',
},
};
expect(
getDefaultFormState(
testValidator,
schema,
{
fromFormData: 'fromFormData',
},
schema,
false,
{ emptyObjectFields: 'skipDefaults' }
)
).toEqual({
localConst: 'local',
RootConst: {
attr1: 1,
attr2: true,
},
RootAndLocalConst: 'FromLocal',
fromFormData: 'fromFormData',
});
});
it('getInnerSchemaForArrayItem() item of type boolean returns empty schema', () => {
expect(getInnerSchemaForArrayItem({ items: [true] }, AdditionalItemsHandling.Ignore, 0)).toEqual({});
});
Expand All @@ -48,6 +104,20 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
foo: 42,
});
});
it('test computeDefaults that is passed a schema with a const property', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
test: {
type: 'string',
const: 'test',
},
},
};
expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({
test: 'test',
});
});
it('test an object with an optional property that has a nested required property', () => {
const schema: RJSFSchema = {
type: 'object',
Expand Down Expand Up @@ -848,6 +918,59 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
requiredProperty: 'foo',
});
});
it('test an object const value populate as field defaults', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
localConst: {
type: 'string',
const: 'local',
},
RootConst: {
type: 'object',
properties: {
attr1: {
type: 'number',
},
attr2: {
type: 'boolean',
},
},
const: {
attr1: 1,
attr2: true,
},
},
fromFormData: {
type: 'string',
default: 'notUsed',
},
RootAndLocalConst: {
type: 'string',
const: 'FromLocal',
},
},
const: {
RootAndLocalConst: 'FromRoot',
},
};
expect(
getObjectDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' },
rawFormData: {
fromFormData: 'fromFormData',
},
})
).toEqual({
localConst: 'local',
RootConst: {
attr1: 1,
attr2: true,
},
RootAndLocalConst: 'FromLocal',
});
});
it('test an object with an additionalProperties', () => {
const schema: RJSFSchema = {
type: 'object',
Expand Down Expand Up @@ -1065,6 +1188,29 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
)
).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']);
});
it('test an array const value populate as defaults', () => {
const schema: RJSFSchema = {
type: 'array',
minItems: 4,
const: ['ConstFromRoot', 'ConstFromRoot'],
items: {
type: 'string',
const: 'Constant',
},
};

expect(
getArrayDefaults(
testValidator,
schema,
{
rootSchema: schema,
includeUndefinedValues: 'excludeObjectChildren',
},
['ConstFromRoot', 'ConstFromRoot']
)
).toEqual(['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']);
});
it('test an array with no defaults', () => {
const schema: RJSFSchema = {
type: 'array',
Expand Down