Skip to content

Commit

Permalink
Fix: Prevent undefined properties at the root level
Browse files Browse the repository at this point in the history
Fixes: rjsf-team#3424 by preventing the inclusion of undefined properties at the root level
- Updated `@rjsf/utils`, making `computeDefaults()` helper in `getDefaultFormState()` to skip adding undefined values when `allowEmptyObject` is set.
  - Updated the `getDefaultFormState()` definition in the `ValidatorType` to add this new option
  - Updated the tests accordingly
- Updated `@rjsf/core`, switching the `excludeObjectChildren` with `allowEmptyObject` in `Form`
- Updated the documentation for `getDefaultFormState()` to add this new option
- Updated the `CHANGELOG.md` accordingly
  - Also added information the changelog for PR rjsf-team#3202
  • Loading branch information
heath-freenome committed Feb 4, 2023
1 parent 65a7be9 commit 261e2f7
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 15 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@ should change the heading of the (upcoming) version to include a major version b
-->
# 5.0.3

## @rjsf/bootstrap-4
- Updated the `AltDateTimeWidget` in `@rjsf/core` to add `className="list-inline-item"` to the `LI` tags

## @rjsf/chakra-ui
- Fixed the `SelectWidget` to allow the proper display of the selected value, fixing [#3422](https://github.com/rjsf-team/react-jsonschema-form/issues/3422)

## @rjsf/core
- Fixed `Form` to pass `allowEmptyObject` to `getDefaultFormState()`, fixing [#3424](https://github.com/rjsf-team/react-jsonschema-form/issues/3424)

## @rjsf/utils
- Updated `getDefaultFormState()` to add a new possible value for `includeUndefinedValues` called `allowEmptyObject` which prevents undefined values within an object but allows an empty object itself.

## Dev / docs / playground
- Updated the `utility-functions` documentation to describe the addition of `allowEmptyObject` to `getDefaultFormState()`'s `includeUndefinedValues` parameter.

# 5.0.2

## @rjsf/utils
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export default class Form<
const formData: T = schemaUtils.getDefaultFormState(
schema,
inputFormData,
"excludeObjectChildren"
"allowEmptyObject"
) as T;
const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,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 | undefined - The current formData, if any, onto which to provide any missing defaults
- [rootSchema]: S | undefined - The root schema, used to primarily to look up `$ref`s
- [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.
- [includeUndefinedValues=false]: boolean | "excludeObjectChildren" | "allowEmptyObject" - Optional flag, if true, cause undefined values to be added as defaults. If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as false when computing defaults for any nested object properties. If "allowEmptyObject", prevents undefined values in this object while allow the object itself to be empty and passing `includeUndefinedValues` as false when computing defaults for any nested object properties.

#### Returns
- T: The resulting `formData` with all the defaults provided
Expand Down
46 changes: 39 additions & 7 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ export enum AdditionalItemsHandling {
Fallback,
}

/** List of choices for `includeUndefinedValues` that should pass false to the recursion
*/
const passAsFalseForIncludeUndefinedValues: Array<string | boolean> = [
"excludeObjectChildren",
"allowEmptyObject",
];

/** List of choices for `includeUndefinedValues` that should allow undefined values to be saved
*/
const allowUndefinedForIncludeUndefinedValues: Array<string | boolean> = [
"excludeObjectChildren",
true,
];

/** Given a `schema` will return an inner schema that for an array item. This is computed differently based on the
* `additionalItems` enum and the value of `idx`. There are four possible returns:
* 1. If `idx` is >= 0, then if `schema.items` is an array the `idx`th element of the array is returned if it is a valid
Expand Down Expand Up @@ -83,13 +97,15 @@ export function getInnerSchemaForArrayItem<
* each level of the schema, recursively, to fill out every level of defaults provided by the schema.
*
* @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the default state is desired
* @param rawSchema - The schema for which the default state is desired
* @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.
* If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested
* object properties.
* If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as
* false when computing defaults for any nested object properties. If "allowEmptyObject", prevents undefined
* values in this object while allow the object itself to be empty and passing `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 @@ -102,7 +118,10 @@ export function computeDefaults<
parentDefaults?: T,
rootSchema: S = {} as S,
rawFormData?: T,
includeUndefinedValues: boolean | "excludeObjectChildren" = false
includeUndefinedValues:
| boolean
| "excludeObjectChildren"
| "allowEmptyObject" = false
): T | T[] | undefined {
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
let schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
Expand Down Expand Up @@ -194,12 +213,22 @@ export function computeDefaults<
get(defaults, [key]),
rootSchema,
get(formData, [key]),
includeUndefinedValues === "excludeObjectChildren"
passAsFalseForIncludeUndefinedValues.includes(
includeUndefinedValues
)
? false
: includeUndefinedValues
);
if (includeUndefinedValues) {
acc[key] = computedDefault;
// Check to make sure an undefined value is allowed, otherwise don't save it
if (
allowUndefinedForIncludeUndefinedValues.includes(
includeUndefinedValues
) ||
computedDefault !== undefined
) {
acc[key] = computedDefault;
}
} else if (isObject(computedDefault)) {
// Store computedDefault if it's a non-empty object (e.g. not {})
if (!isEmpty(computedDefault)) {
Expand Down Expand Up @@ -297,7 +326,10 @@ export default function getDefaultFormState<
theSchema: S,
formData?: T,
rootSchema?: S,
includeUndefinedValues: boolean | "excludeObjectChildren" = false
includeUndefinedValues:
| boolean
| "excludeObjectChildren"
| "allowEmptyObject" = false
) {
if (!isObject(theSchema)) {
throw new Error("Invalid schema: " + theSchema);
Expand Down
11 changes: 8 additions & 3 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,14 +1005,19 @@ 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.
* If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested
* object properties.
* If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as
* false when computing defaults for any nested object properties. If "allowEmptyObject", prevents undefined
* values in this object while allow the object itself to be empty and passing `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 | "excludeObjectChildren"
includeUndefinedValues?:
| boolean
| "excludeObjectChildren"
| "allowEmptyObject"
): 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
50 changes: 47 additions & 3 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ export default function getDefaultFormStateTest(
const schema: RJSFSchema = {
type: "object",
properties: {
optionalProperty: {
optionalNumberProperty: {
type: "number",
},
optionalObjectProperty: {
type: "object",
properties: {
nestedRequiredProperty: {
Expand Down Expand Up @@ -145,9 +148,50 @@ export default function getDefaultFormStateTest(
"excludeObjectChildren"
)
).toEqual({
optionalProperty: {
nestedRequiredProperty: undefined,
optionalNumberProperty: undefined,
optionalObjectProperty: {},
requiredProperty: "foo",
});
});
it("test computeDefaults that is passed an object with an optional object property that has a nested required property and includeUndefinedValues is 'allowEmptyObject'", () => {
const schema: RJSFSchema = {
type: "object",
properties: {
optionalNumberProperty: {
type: "number",
},
optionalObjectProperty: {
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,
"allowEmptyObject"
)
).toEqual({
optionalObjectProperty: {},
requiredProperty: "foo",
});
});
Expand Down

0 comments on commit 261e2f7

Please sign in to comment.