diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ab6f430a..9b3e7f542c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ should change the heading of the (upcoming) version to include a major version b - Update `ArrayFieldItemTemplate` to align buttons with the input field, fixing [#4753](https://github.com/rjsf-team/react-jsonschema-form/pull/4753) +## @rjsf/utils + +- Update `getDefaultFormState()` to add support for `null` defaults for `["null", "object"]` and `["null", "array"]`, fixing [#1581](https://github.com/rjsf-team/react-jsonschema-form/issues/1581) + # 6.0.0-beta.16 ## @rjsf/antd diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 6ae1437cb7..076c3a768d 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -84,6 +84,24 @@ export function getInnerSchemaForArrayItem( + schema: S, + computedDefault: T, +) { + const { default: schemaDefault, type } = schema; + const shouldReturnNullAsDefault = + Array.isArray(type) && type.includes('null') && isEmpty(computedDefault) && schemaDefault === null; + return shouldReturnNullAsDefault ? (null as T) : computedDefault; +} + /** Either add `computedDefault` at `key` into `obj` or not add it based on its value, the value of * `includeUndefinedValues`, the value of `emptyObjectFields` and if its parent field is required. Generally undefined * `computedDefault` values are added only when `includeUndefinedValues` is either true/"excludeObjectChildren". If ` @@ -446,7 +464,7 @@ export function getObjectDefaults = {}, - defaults?: T | T[] | undefined, + defaults?: T | T[], ): T { { const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T; @@ -539,7 +557,7 @@ export function getObjectDefaults(rawSchema, objectDefaults); } } @@ -563,8 +581,8 @@ export function getArrayDefaults = {}, - defaults?: T | T[] | undefined, -): T | T[] | undefined { + defaults?: T[], +): T[] | undefined { const schema: S = rawSchema; const arrayMinItemsStateBehavior = experimental_defaultFormStateBehavior?.arrayMinItems ?? {}; @@ -576,7 +594,7 @@ export function getArrayDefaults false); const isSkipEmptyDefaults = experimental_defaultFormStateBehavior?.emptyObjectFields === 'skipEmptyDefaults'; - const emptyDefault = isSkipEmptyDefaults ? undefined : []; + const emptyDefault: T[] | undefined = isSkipEmptyDefaults ? undefined : []; // Inject defaults into existing array defaults if (Array.isArray(defaults)) { @@ -598,7 +616,7 @@ export function getArrayDefaults(schema); if (neverPopulate) { - defaults = rawFormData; + defaults = rawFormData as typeof defaults; } else { const itemDefaults = rawFormData.map((item: T, idx: number) => { return computeDefaults(validator, schemaItem, { @@ -635,6 +653,7 @@ export function getArrayDefaults(validator, schema, rootSchema) || schema.minItems <= defaultsLength ) { - return defaults ? defaults : emptyDefault; + arrayDefault = defaults ? defaults : emptyDefault; + } else { + const defaultEntries: T[] = (defaults || []) as T[]; + const fillerSchema: S = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert); + const fillerDefault = fillerSchema.default; + + // Calculate filler entries for remaining items (minItems - existing raw data/defaults) + const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () => + computeDefaults(validator, fillerSchema, { + parentDefaults: fillerDefault, + rootSchema, + _recurseList, + experimental_defaultFormStateBehavior, + experimental_customMergeAllOf, + required, + shouldMergeDefaultsIntoFormData, + }), + ) as T[]; + // then fill up the rest with either the item default or empty, up to minItems + arrayDefault = defaultEntries.concat(fillerEntries); } - const defaultEntries: T[] = (defaults || []) as T[]; - const fillerSchema: S = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert); - const fillerDefault = fillerSchema.default; - - // Calculate filler entries for remaining items (minItems - existing raw data/defaults) - const fillerEntries: T[] = Array.from({ length: schema.minItems - defaultsLength }, () => - computeDefaults(validator, fillerSchema, { - parentDefaults: fillerDefault, - rootSchema, - _recurseList, - experimental_defaultFormStateBehavior, - experimental_customMergeAllOf, - required, - shouldMergeDefaultsIntoFormData, - }), - ) as T[]; - // then fill up the rest with either the item default or empty, up to minItems - return defaultEntries.concat(fillerEntries); + return computeDefaultBasedOnSchemaTypeAndDefaults(rawSchema, arrayDefault); } /** Computes the default value based on the schema type. @@ -689,7 +710,7 @@ export function getDefaultBasedOnSchemaType< return getObjectDefaults(validator, rawSchema, computeDefaultsProps, defaults); } case 'array': { - return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults); + return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults as T[]); } } } diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index 420741d59d..4d6f2ced8e 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -1,6 +1,7 @@ import { createSchemaUtils, Experimental_DefaultFormStateBehavior, getDefaultFormState, RJSFSchema } from '../../src'; import { AdditionalItemsHandling, + computeDefaultBasedOnSchemaTypeAndDefaults, computeDefaults, getArrayDefaults, getDefaultBasedOnSchemaType, @@ -1969,7 +1970,119 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType ]); }); }); - + describe('computeDefaultBasedOnSchemaTypeAndDefaults()', () => { + let schema: RJSFSchema; + describe('Object', () => { + beforeAll(() => { + schema = { + type: 'object', + default: null, + }; + }); + it('computedDefaults is undefined', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeUndefined(); + }); + it('computedDefaults is empty object', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, {})).toEqual({}); + }); + it('computedDefaults is non-empty object', () => { + const computedDefault = { foo: 'bar' }; + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault); + }); + }); + describe('Nullable Object', () => { + beforeAll(() => { + schema = { + type: ['null', 'object'], + default: null, + }; + }); + it('computedDefaults is undefined', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeNull(); + }); + it('computedDefaults is empty object', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, {})).toBeNull(); + }); + it('computedDefaults is non-empty object', () => { + const computedDefault = { foo: 'bar' }; + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault); + }); + }); + describe('Array', () => { + beforeAll(() => { + schema = { + type: 'array', + default: null, + items: { type: 'string' }, + }; + }); + it('computedDefaults is undefined', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeUndefined(); + }); + it('computedDefaults is empty object', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, [])).toEqual([]); + }); + it('computedDefaults is non-empty object', () => { + const computedDefault = ['bar']; + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault); + }); + }); + describe('Nullable Array', () => { + beforeAll(() => { + schema = { + type: ['null', 'array'], + default: null, + items: { type: 'string' }, + }; + }); + it('computedDefaults is undefined', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeNull(); + }); + it('computedDefaults is empty object', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, [])).toBeNull(); + }); + it('computedDefaults is non-empty object', () => { + const computedDefault = ['bar']; + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault); + }); + }); + describe('Nullable String', () => { + beforeAll(() => { + schema = { + type: 'string', + default: null, + }; + }); + it('computedDefaults is undefined', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeUndefined(); + }); + it('computedDefaults is empty object', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, '')).toEqual(''); + }); + it('computedDefaults is non-empty object', () => { + const computedDefault = 'bar'; + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault); + }); + }); + describe('Nullable String', () => { + beforeAll(() => { + schema = { + type: ['null', 'string'], + default: null, + }; + }); + it('computedDefaults is undefined', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, undefined)).toBeNull(); + }); + it('computedDefaults is empty object', () => { + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, '')).toBeNull(); + }); + it('computedDefaults is non-empty object', () => { + const computedDefault = 'bar'; + expect(computeDefaultBasedOnSchemaTypeAndDefaults(schema, computedDefault)).toEqual(computedDefault); + }); + }); + }); describe('getValidFormData', () => { let schema: RJSFSchema; it('Test schema with non valid formData for enum property', () => { @@ -5091,12 +5204,12 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect(Array.isArray(result)).toBe(true); // Verify objects are independent instances - (result[0] as any).field = 'test-value-1'; - (result[1] as any).field = 'test-value-2'; - expect((result[2] as any).field).toBeUndefined(); - expect(result[0]).not.toBe(result[1]); - expect(result[1]).not.toBe(result[2]); - expect(result[0]).not.toBe(result[2]); + (result![0] as any).field = 'test-value-1'; + (result![1] as any).field = 'test-value-2'; + expect((result![2] as any).field).toBeUndefined(); + expect(result![0]).not.toBe(result![1]); + expect(result![1]).not.toBe(result![2]); + expect(result![0]).not.toBe(result![2]); }); it('should ensure array items with default values are independent instances', () => { @@ -5125,9 +5238,9 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect(Array.isArray(result)).toBe(true); // Verify objects are independent instances - modifying one shouldn't affect the other - (result[0] as any).field = 'modified-value'; - expect((result[1] as any).field).toBe('default-value'); - expect(result[0]).not.toBe(result[1]); + (result![0] as any).field = 'modified-value'; + expect((result![1] as any).field).toBe('default-value'); + expect(result![0]).not.toBe(result![1]); }); it('should ensure nested objects in arrays are independent instances', () => { @@ -5164,10 +5277,10 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect(Array.isArray(result)).toBe(true); // Verify nested objects are independent instances - (result[0] as any).nested.value = 'modified-nested-value'; - expect((result[1] as any).nested.value).toBe('nested-default'); - expect(result[0]).not.toBe(result[1]); - expect((result[0] as any).nested).not.toBe((result[1] as any).nested); + (result![0] as any).nested.value = 'modified-nested-value'; + expect((result![1] as any).nested.value).toBe('nested-default'); + expect(result![0]).not.toBe(result![1]); + expect((result![0] as any).nested).not.toBe((result![1] as any).nested); }); }); });