Skip to content

Commit

Permalink
feat: arrayMinItems.populate = never
Browse files Browse the repository at this point in the history
  • Loading branch information
alekseyBatuhtin committed Aug 4, 2023
1 parent 49ea69e commit 577d27f
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 17 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
should change the heading of the (upcoming) version to include a major version bump.
-->
# 5.13.0

## @rjsf/utils

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

## Dev / docs / playground

- Updated the `form-props` documentation `arrayMinItems`, added description for `never`.
- Updated the `playground` to add the option for the new `arrayMinItems.populate = 'never'`.

# 5.12.0

## Dev / playground
Expand Down Expand Up @@ -105,7 +117,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
5 changes: 5 additions & 0 deletions packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ const liveSettingsSelectSchema: RJSFSchema = {
title: 'Ignore minItems unless field is required',
enum: ['requiredOnly'],
},
{
type: 'string',
title: 'Ignore populating arrays even if settled minItems',
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: ['raw0'] },
experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'never' } },
})
).toStrictEqual({ nonRequiredArray: ['raw0'] });
});

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

0 comments on commit 577d27f

Please sign in to comment.