diff --git a/CHANGELOG.md b/CHANGELOG.md index 56780b1873..194905b6dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,16 @@ should change the heading of the (upcoming) version to include a major version b --> # 5.0.0-beta.14 +## @rjsf/antd +- No longer render extra 0 for array without errors, fixing [#3233](https://github.com/rjsf-team/react-jsonschema-form/issues/3233) + ## @rjsf/core - Added `ref` definition to `ThemeProps` fixing [#2135](https://github.com/rjsf-team/react-jsonschema-form/issues/2135) +## @rjsf/utils +- Updated `computedDefaults` (used by `getDefaultFormState`) to skip saving the computed default if it's an empty object unless `includeUndefinedValues` is truthy, fixing [#2150](https://github.com/rjsf-team/react-jsonschema-form/issues/2150) and [#2708](https://github.com/rjsf-team/react-jsonschema-form/issues/2708) +- Expanded the `getDefaultFormState` util's `includeUndefinedValues` prop to accept a boolean or `"excludeObjectChildren"` if it does not want to include undefined values in nested objects + # 5.0.0-beta.13 ## @rjsf/playground @@ -34,9 +41,6 @@ should change the heading of the (upcoming) version to include a major version b - For JSON Schemas with `$id`s, use a pre-compiled Ajv validation function when available. - No longer fail to validate inner schemas with `$id`s, fixing [#2821](https://github.com/rjsf-team/react-jsonschema-form/issues/2181). -## @rjsf/antd -- No longer render extra 0 for array without errors, fixing [#3233](https://github.com/rjsf-team/react-jsonschema-form/issues/3233) - # 5.0.0-beta.12 ## @rjsf/antd diff --git a/docs/api-reference/utility-functions.md b/docs/api-reference/utility-functions.md index 5c8636ee98..8448288ef9 100644 --- a/docs/api-reference/utility-functions.md +++ b/docs/api-reference/utility-functions.md @@ -415,7 +415,7 @@ Returns the superset of `formData` that includes the given set updated to includ - theSchema: S - The schema for which the default state is desired - [formData]: T - The current formData, if any, onto which to provide any missing defaults - [rootSchema]: S - The root schema, used to primarily to look up `$ref`s -- [includeUndefinedValues=false]: boolean - Optional flag, if true, cause undefined values to be added as defaults +- [includeUndefinedValues=false]: boolean | "excludeObjectChildren" - Optional flag, if true, cause undefined values to be added as defaults. If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested object properties. #### Returns - T: The resulting `formData` with all the defaults provided diff --git a/packages/core/src/components/fields/MultiSchemaField.tsx b/packages/core/src/components/fields/MultiSchemaField.tsx index 99b986029d..75d58cc23f 100644 --- a/packages/core/src/components/fields/MultiSchemaField.tsx +++ b/packages/core/src/components/fields/MultiSchemaField.tsx @@ -129,11 +129,13 @@ class AnyOfField< } } } - // Call getDefaultFormState to make sure defaults are populated on change. + // Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren" + // so that only the root objects themselves are created without adding undefined children properties onChange( schemaUtils.getDefaultFormState( options[selectedOption], - newFormData + newFormData, + "excludeObjectChildren" ) as T, undefined, this.getFieldId() diff --git a/packages/core/test/oneOf_test.js b/packages/core/test/oneOf_test.js index cdee271ea3..92df08f3c7 100644 --- a/packages/core/test/oneOf_test.js +++ b/packages/core/test/oneOf_test.js @@ -659,7 +659,7 @@ describe("oneOf", () => { sinon.assert.calledWithMatch( onChange.lastCall, { - formData: { lorem: undefined, ipsum: {} }, + formData: { ipsum: {} }, }, "root__oneof_select" ); diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index ef269b191b..a33d76caef 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -81,13 +81,15 @@ class SchemaUtils< * * @param schema - The schema for which the default state is desired * @param [formData] - The current formData, if any, onto which to provide any missing defaults - * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults + * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults. + * If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested + * object properties. * @returns - The resulting `formData` with all the defaults provided */ getDefaultFormState( schema: S, formData?: T, - includeUndefinedValues = false + includeUndefinedValues: boolean | "excludeObjectChildren" = false ): T | T[] | undefined { return getDefaultFormState( this.validator, diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 8208e445bf..a67ee721a5 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -86,7 +86,9 @@ export function getInnerSchemaForArrayItem< * @param [parentDefaults] - Any defaults provided by the parent field in the schema * @param [rootSchema] - The options root schema, used to primarily to look up `$ref`s * @param [rawFormData] - The current formData, if any, onto which to provide any missing defaults - * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults + * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults. + * If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested + * object properties. * @returns - The resulting `formData` with all the defaults provided */ export function computeDefaults< @@ -98,7 +100,7 @@ export function computeDefaults< parentDefaults?: T, rootSchema: S = {} as S, rawFormData?: T, - includeUndefinedValues = false + includeUndefinedValues: boolean | "excludeObjectChildren" = false ): T | T[] | undefined { const formData = isObject(rawFormData) ? rawFormData : {}; // Compute the defaults recursively: give highest priority to deepest nodes. @@ -187,9 +189,19 @@ export function computeDefaults< get(defaults, [key]), rootSchema, get(formData, [key]), - includeUndefinedValues + includeUndefinedValues === "excludeObjectChildren" + ? false + : includeUndefinedValues ); - if (includeUndefinedValues || computedDefault !== undefined) { + if (includeUndefinedValues) { + acc[key] = computedDefault; + } else if (isObject(computedDefault)) { + // Store computedDefault if it's a non-empty object (e.g. not {}) + if (!isEmpty(computedDefault)) { + acc[key] = computedDefault; + } + } else if (computedDefault !== undefined) { + // Store computedDefault if it's a defined primitive (e.g. true) acc[key] = computedDefault; } return acc; @@ -261,7 +273,9 @@ export function computeDefaults< * @param theSchema - The schema for which the default state is desired * @param [formData] - The current formData, if any, onto which to provide any missing defaults * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s - * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults + * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults. + * If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested + * object properties. * @returns - The resulting `formData` with all the defaults provided */ export default function getDefaultFormState< @@ -272,7 +286,7 @@ export default function getDefaultFormState< theSchema: S, formData?: T, rootSchema?: S, - includeUndefinedValues = false + includeUndefinedValues: boolean | "excludeObjectChildren" = false ) { if (!isObject(theSchema)) { throw new Error("Invalid schema: " + theSchema); diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 6e08850770..7a2bf2ad17 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -951,13 +951,15 @@ export interface SchemaUtilsType< * * @param schema - The schema for which the default state is desired * @param [formData] - The current formData, if any, onto which to provide any missing defaults - * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults + * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults. + * If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested + * object properties. * @returns - The resulting `formData` with all the defaults provided */ getDefaultFormState( schema: S, formData?: T, - includeUndefinedValues?: boolean + includeUndefinedValues?: boolean | "excludeObjectChildren" ): T | T[] | undefined; /** Determines whether the combination of `schema` and `uiSchema` properties indicates that the label for the `schema` * should be displayed in a UI. diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index bca6885721..aabd14fd84 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -43,6 +43,114 @@ export default function getDefaultFormStateTest( foo: 42, }); }); + it("test computeDefaults that is passed an object with an optional object property that has a nested required property", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + optionalProperty: { + type: "object", + properties: { + nestedRequiredProperty: { + type: "string", + }, + }, + required: ["nestedRequiredProperty"], + }, + requiredProperty: { + type: "string", + default: "foo", + }, + }, + required: ["requiredProperty"], + }; + expect( + computeDefaults(testValidator, schema, undefined, schema) + ).toEqual({ requiredProperty: "foo" }); + }); + it("test computeDefaults that is passed an object with an optional object property that has a nested required property and includeUndefinedValues", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + optionalProperty: { + type: "object", + properties: { + nestedRequiredProperty: { + type: "object", + properties: { + undefinedProperty: { + type: "string", + }, + }, + }, + }, + required: ["nestedRequiredProperty"], + }, + requiredProperty: { + type: "string", + default: "foo", + }, + }, + required: ["requiredProperty"], + }; + expect( + computeDefaults( + testValidator, + schema, + undefined, + schema, + undefined, + true + ) + ).toEqual({ + optionalProperty: { + nestedRequiredProperty: { + undefinedProperty: undefined, + }, + }, + requiredProperty: "foo", + }); + }); + it("test computeDefaults that is passed an object with an optional object property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + optionalProperty: { + type: "object", + properties: { + nestedRequiredProperty: { + type: "object", + properties: { + undefinedProperty: { + type: "string", + }, + }, + }, + }, + required: ["nestedRequiredProperty"], + }, + requiredProperty: { + type: "string", + default: "foo", + }, + }, + required: ["requiredProperty"], + }; + expect( + computeDefaults( + testValidator, + schema, + undefined, + schema, + undefined, + "excludeObjectChildren" + ) + ).toEqual({ + optionalProperty: { + nestedRequiredProperty: undefined, + }, + requiredProperty: "foo", + }); + }); }); describe("root default", () => { it("should map root schema default to form state, if any", () => { diff --git a/packages/validator-ajv6/src/validator.ts b/packages/validator-ajv6/src/validator.ts index 0970379ed1..7e3ecb6114 100644 --- a/packages/validator-ajv6/src/validator.ts +++ b/packages/validator-ajv6/src/validator.ts @@ -259,17 +259,9 @@ export default class AJV6Validator customValidate?: CustomValidator, transformErrors?: ErrorTransformer ): ValidationData { - // Include form data with undefined values, which is required for validation. const rootSchema = schema; - const newFormData = getDefaultFormState( - this, - schema, - formData, - rootSchema, - true - ) as T; - const rawErrors = this.rawValidation(schema, newFormData); + const rawErrors = this.rawValidation(schema, formData); const { validationError } = rawErrors; let errors = this.transformRJSFValidationErrors(rawErrors.errors); @@ -302,6 +294,15 @@ export default class AJV6Validator return { errors, errorSchema }; } + // Include form data with undefined values, which is required for custom validation. + const newFormData = getDefaultFormState( + this, + schema, + formData, + rootSchema, + true + ) as T; + const errorHandler = customValidate( newFormData, this.createErrorHandler(newFormData) diff --git a/packages/validator-ajv8/src/validator.ts b/packages/validator-ajv8/src/validator.ts index 957e47b363..d0f314aee9 100644 --- a/packages/validator-ajv8/src/validator.ts +++ b/packages/validator-ajv8/src/validator.ts @@ -292,16 +292,7 @@ export default class AJV8Validator< customValidate?: CustomValidator, transformErrors?: ErrorTransformer ): ValidationData { - // Include form data with undefined values, which is required for validation. - const newFormData = getDefaultFormState( - this, - schema, - formData, - schema, - true - ) as T; - - const rawErrors = this.rawValidation(schema, newFormData); + const rawErrors = this.rawValidation(schema, formData); const { validationError: invalidSchemaError } = rawErrors; let errors = this.transformRJSFValidationErrors(rawErrors.errors); @@ -327,6 +318,15 @@ export default class AJV8Validator< return { errors, errorSchema }; } + // Include form data with undefined values, which is required for custom validation. + const newFormData = getDefaultFormState( + this, + schema, + formData, + schema, + true + ) as T; + const errorHandler = customValidate( newFormData, this.createErrorHandler(newFormData)