Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 5.18.0

## @rjsf/utils

- Added a new `skipEmptyDefault` option in `emptyObjectFields`, fixing [#3880](https://github.com/rjsf-team/react-jsonschema-form/issues/3880)

## Dev / docs / playground

- Updated the documentation to describe how to use the `skipEmptyDefault` option.

# 5.17.1

## @rjsf/chakra-ui
Expand Down
1 change: 1 addition & 0 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Optional enumerated flag controlling how empty object fields are populated, defa
| `populateAllDefaults` | Legacy behavior - set default when there is a primitive value, an non-empty object field, or the field itself is required |
| `populateRequiredDefaults` | Only sets default when a value is an object and its parent field is required, or it is a primitive value and it is required |
| `skipDefaults` | Does not set defaults |
| `skipEmptyDefaults` | Does not set an empty default. It will still apply the default value if a default property is defined in your schema |

```tsx
import { RJSFSchema } from '@rjsf/utils';
Expand Down
5 changes: 5 additions & 0 deletions packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ const liveSettingsSelectSchema: RJSFSchema = {
'Assign value to formData when default is an object and parent is required, or default is primitive and is required',
enum: ['populateRequiredDefaults'],
},
{
type: 'string',
title: 'Assign value to formData when only default is set',
enum: ['skipEmptyDefaults'],
},
{
type: 'string',
title: 'Does not set defaults',
Expand Down
24 changes: 18 additions & 6 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,17 @@ function maybeAddDefaultToObject<T = any>(
// If isParentRequired is undefined, then we are at the root level of the schema so defer to the requiredness of
// the field key itself in the `requiredField` list
const isSelfOrParentRequired = isParentRequired === undefined ? requiredFields.includes(key) : isParentRequired;
// Store computedDefault if it's a non-empty object(e.g. not {}) and satisfies certain conditions

// If emptyObjectFields 'skipEmptyDefaults' store computedDefault if it's a non-empty object(e.g. not {})
if (emptyObjectFields === 'skipEmptyDefaults') {
if (!isEmpty(computedDefault)) {
obj[key] = computedDefault;
}
}
// Else store computedDefault if it's a non-empty object(e.g. not {}) and satisfies certain conditions
// Condition 1: If computedDefault is not empty or if the key is a required field
// Condition 2: If the parent object is required or emptyObjectFields is not 'populateRequiredDefaults'
if (
else if (
(!isEmpty(computedDefault) || requiredFields.includes(key)) &&
(isSelfOrParentRequired || emptyObjectFields !== 'populateRequiredDefaults')
) {
Expand All @@ -122,9 +129,11 @@ function maybeAddDefaultToObject<T = any>(
} else if (
// Store computedDefault if it's a defined primitive (e.g., true) and satisfies certain conditions
// Condition 1: computedDefault is not undefined
// Condition 2: If emptyObjectFields is 'populateAllDefaults' or if the key is a required field
// Condition 2: If emptyObjectFields is 'populateAllDefaults' or 'skipEmptyDefaults) or if the key is a required field
computedDefault !== undefined &&
(emptyObjectFields === 'populateAllDefaults' || requiredFields.includes(key))
(emptyObjectFields === 'populateAllDefaults' ||
emptyObjectFields === 'skipEmptyDefaults' ||
requiredFields.includes(key))
) {
obj[key] = computedDefault;
}
Expand Down Expand Up @@ -340,6 +349,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
case 'array': {
const neverPopulate = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'never';
const ignoreMinItemsFlagSet = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'requiredOnly';
const isSkipEmptyDefaults = experimental_defaultFormStateBehavior?.emptyObjectFields === 'skipEmptyDefaults';

const emptyDefault = isSkipEmptyDefaults ? undefined : [];

// Inject defaults into existing array defaults
if (Array.isArray(defaults)) {
Expand Down Expand Up @@ -375,7 +387,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
}

if (neverPopulate) {
return defaults ?? [];
return defaults ?? emptyDefault;
}
if (ignoreMinItemsFlagSet && !required) {
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
Expand All @@ -389,7 +401,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
isMultiSelect<T, S, F>(validator, schema, rootSchema) ||
schema.minItems <= defaultsLength
) {
return defaults ? defaults : [];
return defaults ? defaults : emptyDefault;
}

const defaultEntries: T[] = (defaults || []) as T[];
Expand Down
3 changes: 2 additions & 1 deletion packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ export type Experimental_DefaultFormStateBehavior = {
* - `populateRequiredDefaults`: Only sets default when a value is an object and its parent field is required, or it
* is a primitive value and it is required |
* - `skipDefaults`: Does not set defaults |
* - `skipEmptyDefaults`: Does not set an empty default. It will still apply the default value if a default property is defined in your schema. |
*/
emptyObjectFields?: 'populateAllDefaults' | 'populateRequiredDefaults' | 'skipDefaults';
emptyObjectFields?: 'populateAllDefaults' | 'populateRequiredDefaults' | 'skipDefaults' | 'skipEmptyDefaults';
/**
* Optional flag to compute the default form state using allOf and if/then/else schemas. Defaults to `skipDefaults'.
*/
Expand Down
257 changes: 257 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,9 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
});
});
});
/**
* emptyObjectFields options tests
*/
describe('default form state behavior: emptyObjectFields = "populateRequiredDefaults"', () => {
it('test an object with an optional property that has a nested required property', () => {
const schema: RJSFSchema = {
Expand Down Expand Up @@ -1106,6 +1109,260 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
});
});
});
describe('default form state behavior: emptyObjectFields = "skipEmptyDefaults"', () => {
it('test an object with an optional 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, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({ requiredProperty: 'foo' });
});
it('test an object with a nested required property in a ref', () => {
const schema: RJSFSchema = {
type: 'object',
definitions: {
nestedRequired: {
properties: {
nested: {
type: 'string',
default: 'foo',
},
},
required: ['nested'],
},
},
properties: {
nestedRequiredProperty: {
$ref: '#/definitions/nestedRequired',
},
requiredProperty: {
type: 'string',
default: 'foo',
},
},
required: ['requiredProperty', 'nestedRequiredProperty'],
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({ requiredProperty: 'foo', nestedRequiredProperty: { nested: 'foo' } });
});
it('test an object with a nested optional property in a ref', () => {
const schema: RJSFSchema = {
type: 'object',
definitions: {
nestedOptional: {
properties: {
nested: {
type: 'string',
default: 'foo',
},
},
},
},
properties: {
nestedOptionalProperty: {
$ref: '#/definitions/nestedOptional',
},
requiredProperty: {
type: 'string',
default: 'foo',
},
},
required: ['requiredProperty', 'nestedOptionalProperty'],
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({
nestedOptionalProperty: {
nested: 'foo',
},
requiredProperty: 'foo',
});
});
it('test an object with an optional property that has a nested required property with default', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
optionalProperty: {
type: 'object',
properties: {
nestedRequiredProperty: {
type: 'string',
default: '',
},
},
required: ['nestedRequiredProperty'],
},
requiredProperty: {
type: 'string',
default: 'foo',
},
},
required: ['requiredProperty'],
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({
optionalProperty: {
nestedRequiredProperty: '',
},
requiredProperty: 'foo',
});
});
it('test an object with an optional 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, {
rootSchema: schema,
includeUndefinedValues: true,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({
optionalProperty: {
nestedRequiredProperty: {
undefinedProperty: undefined,
},
},
requiredProperty: 'foo',
});
});
it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => {
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, {
rootSchema: schema,
includeUndefinedValues: 'excludeObjectChildren',
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({
optionalNumberProperty: undefined,
optionalObjectProperty: {},
requiredProperty: 'foo',
});
});
it('test an optional array without default value, an optional array with a default value, and a required array', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
array: {
type: 'array',
title: 'Multiple Select Input',
items: {
type: 'string',
enum: ['option1', 'option2'],
},
uniqueItems: true,
},
arrayWithDefault: {
type: 'array',
title: 'Required multiple Select Input',
items: {
type: 'string',
enum: ['option1', 'option2'],
},
uniqueItems: true,
default: ['option1'],
},
arrayRequired: {
type: 'array',
title: 'Required multiple Select Input',
items: {
type: 'string',
enum: ['option1', 'option2'],
},
uniqueItems: true,
},
},
required: ['arrayRequired'],
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
includeUndefinedValues: 'excludeObjectChildren',
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipEmptyDefaults' },
})
).toEqual({ arrayWithDefault: ['option1'] });
});
});

describe('root default', () => {
it('should map root schema default to form state, if any', () => {
expect(
Expand Down