Skip to content

Commit

Permalink
fix(utils): omit computedDefault of empty required properties nested …
Browse files Browse the repository at this point in the history
…in optional parent property (rjsf-team#3287)

* fix(utils): omit computedDefault of empty required properties nested in optional parent property

* responding to feedback - updated once to excludeObjectChildren, updated changelog

* Add comment into MultiSchemaField to explain reason for passing includeUndefinedValues prop as "excludeObjectChildren"
  • Loading branch information
zmagauina-fn authored and shijistar committed Jun 8, 2023
1 parent edddfce commit b1b7338
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 36 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/components/fields/MultiSchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/oneOf_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ describe("oneOf", () => {
sinon.assert.calledWithMatch(
onChange.lastCall,
{
formData: { lorem: undefined, ipsum: {} },
formData: { ipsum: {} },
},
"root__oneof_select"
);
Expand Down
6 changes: 4 additions & 2 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, S>(
this.validator,
Expand Down
26 changes: 20 additions & 6 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<
Expand All @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
108 changes: 108 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
19 changes: 10 additions & 9 deletions packages/validator-ajv6/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,17 +259,9 @@ export default class AJV6Validator<T = any>
customValidate?: CustomValidator<T>,
transformErrors?: ErrorTransformer
): ValidationData<T> {
// Include form data with undefined values, which is required for validation.
const rootSchema = schema;
const newFormData = getDefaultFormState<T, RJSFSchema>(
this,
schema,
formData,
rootSchema,
true
) as T;

const rawErrors = this.rawValidation<ErrorObject>(schema, newFormData);
const rawErrors = this.rawValidation<ErrorObject>(schema, formData);
const { validationError } = rawErrors;
let errors = this.transformRJSFValidationErrors(rawErrors.errors);

Expand Down Expand Up @@ -302,6 +294,15 @@ export default class AJV6Validator<T = any>
return { errors, errorSchema };
}

// Include form data with undefined values, which is required for custom validation.
const newFormData = getDefaultFormState<T, RJSFSchema>(
this,
schema,
formData,
rootSchema,
true
) as T;

const errorHandler = customValidate(
newFormData,
this.createErrorHandler(newFormData)
Expand Down
20 changes: 10 additions & 10 deletions packages/validator-ajv8/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,7 @@ export default class AJV8Validator<
customValidate?: CustomValidator<T>,
transformErrors?: ErrorTransformer
): ValidationData<T> {
// Include form data with undefined values, which is required for validation.
const newFormData = getDefaultFormState<T>(
this,
schema,
formData,
schema,
true
) as T;

const rawErrors = this.rawValidation<ErrorObject>(schema, newFormData);
const rawErrors = this.rawValidation<ErrorObject>(schema, formData);
const { validationError: invalidSchemaError } = rawErrors;
let errors = this.transformRJSFValidationErrors(rawErrors.errors);

Expand All @@ -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<T>(
this,
schema,
formData,
schema,
true
) as T;

const errorHandler = customValidate(
newFormData,
this.createErrorHandler(newFormData)
Expand Down

0 comments on commit b1b7338

Please sign in to comment.