Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature #3796 arrayMinItems.populate = never #3809

Merged
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
19 changes: 13 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,24 @@ should change the heading of the (upcoming) version to include a major version b
-->
# 5.12.0

## Dev / playground
## @rjsf/utils

- Experimental feature:
- Added `experimental_defaultFormStateBehavior = { arrayMinItems: { populate: 'never' } }` (feature [#3796](https://github.com/rjsf-team/react-jsonschema-form/issues/3796))

## @rjsf/validator-ajv8

- Exposing new function `compileSchemaValidatorsCode` to allow creating precompiled validator without a file. This is useful in case when precompiled validator is to be created dynamically. [#3793](https://github.com/rjsf-team/react-jsonschema-form/pull/3793)

## Dev / docs / playground

- update playground vite config to use sources directly, allowing to reload changes in it without additional build step
- moving from `dts-cli` to use individual dev tools directly, updating package publish config
- tsc for generating type definitions and esm modules
- esbuild for CJS bundle
- rollup for UMD bundle

## @rjsf/validator-ajv8

- Exposing new function `compileSchemaValidatorsCode` to allow creating precompiled validator without a file. This is useful in case when precompiled validator is to be created dynamically. [#3793](https://github.com/rjsf-team/react-jsonschema-form/pull/3793)
- Updated the `form-props` documentation `arrayMinItems`, added description for `never`.
- Updated the `playground` to add the option for the new `arrayMinItems.populate = 'never'`.

# 5.11.2

Expand Down Expand Up @@ -105,7 +112,7 @@ should change the heading of the (upcoming) version to include a major version b

- Updated the `utility-functions` documentation to add the new `mergeExtraArrayDefaults` flag for the `mergeDefaultWithFormData()` function
- Updated the `form-props` documentation to update the `arrayMinItems` documentation for the new object behavior
- Updated the `playground` to add a checkbox for the new `arrayMinItems.mergeExtraDefaults` flag
- Updated the `playground` to add a checkbox for the new `arrayMinItems.mergeExtraDefaults` flag

# 5.8.2

Expand Down
9 changes: 5 additions & 4 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ When not specified, it defaults to `{ populate: 'all', mergeExtraDefaults: false

Optional enumerated flag controlling how array minItems are populated, defaulting to `all`:

| Flag Value | Description |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `all` | Legacy behavior - populate minItems entries with default values initially and include empty array when no values have been defined. |
| `requiredOnly` | Ignore `minItems` on a field when calculating defaults unless the field is required. |
| Flag Value | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `all` | Legacy behavior - populate minItems entries with default values initially and include empty array when no values have been defined. |
| `requiredOnly` | Ignore `minItems` on a field when calculating defaults unless the field is required. |
| `never` | Ignore `minItems` on a field when calculating defaults for required and non-required. Value will set only if defined `default` and from `formData` |

#### `arrayMinItems.mergeExtraDefaults`

Expand Down
7 changes: 6 additions & 1 deletion packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ const liveSettingsSelectSchema: RJSFSchema = {
},
{
type: 'string',
title: 'Ignore minItems unless field is required',
title: 'Only populate minItems with default values when field is required',
enum: ['requiredOnly'],
},
{
type: 'string',
title: 'Never populate minItems with default values',
enum: ['never'],
},
],
},
mergeExtraDefaults: {
Expand Down
31 changes: 20 additions & 11 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
return objectDefaults;
}
case 'array': {
const neverPopulate = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'never';
const ignoreMinItemsFlagSet = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'requiredOnly';

// Inject defaults into existing array defaults
if (Array.isArray(defaults)) {
defaults = defaults.map((item, idx) => {
Expand All @@ -338,19 +341,25 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
// Deeply inject defaults into already existing form data
if (Array.isArray(rawFormData)) {
const schemaItem: S = getInnerSchemaForArrayItem<S>(schema);
defaults = rawFormData.map((item: T, idx: number) => {
return computeDefaults<T, S, F>(validator, schemaItem, {
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
rawFormData: item,
parentDefaults: get(defaults, [idx]),
required,
});
}) as T[];
if (neverPopulate) {
defaults = rawFormData;
} else {
defaults = rawFormData.map((item: T, idx: number) => {
return computeDefaults<T, S, F>(validator, schemaItem, {
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
rawFormData: item,
parentDefaults: get(defaults, [idx]),
required,
});
}) as T[];
}
}

const ignoreMinItemsFlagSet = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'requiredOnly';
if (neverPopulate) {
return defaults ?? [];
}
if (ignoreMinItemsFlagSet && !required) {
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
// return form data/defaults
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 @@ -38,8 +38,9 @@ export type Experimental_ArrayMinItems = {
* - `all`: Legacy behavior, populate minItems entries with default values initially and include an empty array when
* no values have been defined.
* - `requiredOnly`: Ignore `minItems` on a field when calculating defaults unless the field is required.
* - `never`: Ignore `minItems` on a field even the field is required.
*/
populate?: 'all' | 'requiredOnly';
populate?: 'all' | 'requiredOnly' | 'never';
/** When `formData` is provided and does not contain `minItems` worth of data, this flag (`false` by default) controls
* whether the extra data provided by the defaults is appended onto the existing `formData` items to ensure the
* `minItems` condition is met. When false (legacy behavior), only the `formData` provided is merged into the default
Expand Down
220 changes: 220 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,226 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
).toEqual({ requiredArray: ['default0', 'default0'] });
});
});
describe('default form state behavior: arrayMinItems.populate = "never"', () => {
it('should not be filled if minItems defined and required', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
requiredArray: {
type: 'array',
items: { type: 'string' },
minItems: 1,
},
},
required: ['requiredArray'],
};

expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ requiredArray: [] });
});
it('should not be filled if minItems defined and non required', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
items: { type: 'string' },
minItems: 1,
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: [] });
});

it('should be filled with default values if required', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
requiredArray: {
type: 'array',
default: ['raw0'],
items: { type: 'string' },
minItems: 1,
},
},
required: ['requiredArray'],
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ requiredArray: ['raw0'] });
});

it('should be filled with default values if non required', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
default: ['raw0'],
items: { type: 'string' },
minItems: 1,
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: ['raw0'] });
});

it('should be filled with default values partly and not fill others', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
default: ['raw0'],
items: { type: 'string' },
minItems: 2,
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
rawFormData: { nonRequiredArray: ['raw1'] },
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: ['raw1'] });
});

it('should not add items to formData', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
items: { type: 'string' },
minItems: 2,
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
rawFormData: { nonRequiredArray: ['not add'] },
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: ['not add'] });
});

it('should be empty array if minItems not defined and required', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
requiredArray: {
type: 'array',
items: { type: 'string' },
},
},
required: ['requiredArray'],
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ requiredArray: [] });
});
it('should be empty array if minItems not defined and non required', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
items: { type: 'string' },
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: [] });
});

it('injecting data should be stopped at the top level of tree', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
minItems: 2,
items: {
type: 'object',
properties: {
nestedValue: { type: 'string' },
nestedArray: { type: 'array', items: { type: 'string' } },
},
},
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: [] });
});
it('no injecting for childs', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
nonRequiredArray: {
type: 'array',
minItems: 2,
items: {
type: 'object',
properties: {
nestedValue: { type: 'string' },
nestedArray: { type: 'array', minItems: 3, items: { type: 'string' } },
},
},
},
},
};
expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
rawFormData: {
nonRequiredArray: [
{
nestedArray: ['raw0'],
},
],
},
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({
nonRequiredArray: [
{
nestedArray: ['raw0'],
},
],
});
});
});
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