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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 47 additions & 26 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ export function getInnerSchemaForArrayItem<S extends StrictRJSFSchema = RJSFSche
return {} as S;
}

/** Checks if the given `schema` contains the `null` type along with another type AND if the `default` contained within
* the schema is `null` AND the `computedDefault` is empty. If all of those conditions are true, then the `schema`'s
* default should be `null` rather than `computedDefault`.
*
* @param schema - The schema to inspect
* @param computedDefault - The computed default for the schema
* @returns - Flag indicating whether a null should be returned instead of the computedDefault
*/
export function computeDefaultBasedOnSchemaTypeAndDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema>(
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 `
Expand Down Expand Up @@ -446,7 +464,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
required,
shouldMergeDefaultsIntoFormData,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T | T[] | undefined,
defaults?: T | T[],
): T {
{
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
Expand Down Expand Up @@ -539,7 +557,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
);
});
}
return objectDefaults;
return computeDefaultBasedOnSchemaTypeAndDefaults<T, S>(rawSchema, objectDefaults);
}
}

Expand All @@ -563,8 +581,8 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
required,
shouldMergeDefaultsIntoFormData,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T | T[] | undefined,
): T | T[] | undefined {
defaults?: T[],
): T[] | undefined {
const schema: S = rawSchema;

const arrayMinItemsStateBehavior = experimental_defaultFormStateBehavior?.arrayMinItems ?? {};
Expand All @@ -576,7 +594,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
const computeSkipPopulate = arrayMinItemsStateBehavior?.computeSkipPopulate ?? (() => 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)) {
Expand All @@ -598,7 +616,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
if (Array.isArray(rawFormData)) {
const schemaItem: S = getInnerSchemaForArrayItem<S>(schema);
if (neverPopulate) {
defaults = rawFormData;
defaults = rawFormData as typeof defaults;
} else {
const itemDefaults = rawFormData.map((item: T, idx: number) => {
return computeDefaults<T, S, F>(validator, schemaItem, {
Expand Down Expand Up @@ -635,34 +653,37 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
}
}

let arrayDefault: T[] | undefined;
const defaultsLength = Array.isArray(defaults) ? defaults.length : 0;
if (
!schema.minItems ||
isMultiSelect<T, S, F>(validator, schema, rootSchema, experimental_customMergeAllOf) ||
computeSkipPopulate<T, S, F>(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<S>(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<any, S, F>(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<S>(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<any, S, F>(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<T[] | undefined, S>(rawSchema, arrayDefault);
}

/** Computes the default value based on the schema type.
Expand All @@ -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[]);
}
}
}
Expand Down
141 changes: 127 additions & 14 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSchemaUtils, Experimental_DefaultFormStateBehavior, getDefaultFormState, RJSFSchema } from '../../src';
import {
AdditionalItemsHandling,
computeDefaultBasedOnSchemaTypeAndDefaults,
computeDefaults,
getArrayDefaults,
getDefaultBasedOnSchemaType,
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
Expand Down