Skip to content

Commit c09cc1a

Browse files
Implement feature-flagged support for ignoring of optional array fields with minItems set (rjsf-team#3604)
1 parent 814c1d9 commit c09cc1a

File tree

11 files changed

+491
-147
lines changed

11 files changed

+491
-147
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ should change the heading of the (upcoming) version to include a major version b
2626

2727
- Updated the `MultiSchemaField` to use the new `getDiscriminatorFieldFromSchema()` API
2828

29+
- Added new `experimental_defaultFormStateBehavior` prop to `Form` to specify alternate behavior when dealing with the rendering of array fields where `minItems` is set but field is not `required` (fixes [3363](https://github.com/rjsf-team/react-jsonschema-form/issues/3363)) ([3604](https://github.com/rjsf-team/react-jsonschema-form/pull/3604))
30+
2931
## @rjsf/fluent-ui
3032

3133
- Added support for `additionalProperties` to fluent-ui theme, fixing [#2777](https://github.com/rjsf-team/react-jsonschema-form/issues/2777).
@@ -36,10 +38,12 @@ should change the heading of the (upcoming) version to include a major version b
3638
- Updated `getDefaultFormState()` and `toPathSchema()` to use `getDiscriminatorFieldFromSchema()` to provide a discriminator field to `getClosestMatchingOption()` calls.
3739
- Refactored the `retrieveSchema()` internal API functions to support implementing an internal `schemaParser()` API for use in precompiling schemas, in support of [#3543](https://github.com/rjsf-team/react-jsonschema-form/issues/3543)
3840
- Fixed `toPathSchema()` to handle `properties` in an object along with `anyOf`/`oneOf`, fixing [#3628](https://github.com/rjsf-team/react-jsonschema-form/issues/3628) and [#1628](https://github.com/rjsf-team/react-jsonschema-form/issues/1628)
41+
- Refactored optional parameters for `computeDefaults()` into destructured props object to reduce clutter when only specifying later of the optional argument ([3604](https://github.com/rjsf-team/react-jsonschema-form/pull/3604))
3942

4043
## Dev / docs / playground
4144

4245
- Added documentation to `custom-templates` describing how to extend the `BaseInputTemplate`
46+
- Added **minItems behavior for array field** live setting ([3604](https://github.com/rjsf-team/react-jsonschema-form/pull/3604))
4347

4448
# 5.6.2
4549

packages/core/src/components/Form.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ValidationData,
3333
validationDataMerge,
3434
ValidatorType,
35+
Experimental_DefaultFormStateBehavior,
3536
} from '@rjsf/utils';
3637
import _get from 'lodash/get';
3738
import _isEmpty from 'lodash/isEmpty';
@@ -181,6 +182,9 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
181182
* to put the second parameter before the first in its translation.
182183
*/
183184
translateString?: Registry['translateString'];
185+
/** Optional configuration object with flags, if provided, allows users to override default form state behavior
186+
* Currently only affecting minItems on array fields */
187+
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior;
184188
// Private
185189
/**
186190
* _internalFormWrapper is currently used by the semantic-ui theme to provide a custom wrapper around `<Form />`
@@ -305,9 +309,16 @@ export default class Form<
305309
const liveValidate = 'liveValidate' in props ? props.liveValidate : this.props.liveValidate;
306310
const mustValidate = edit && !props.noValidate && liveValidate;
307311
const rootSchema = schema;
312+
const experimental_defaultFormStateBehavior =
313+
'experimental_defaultFormStateBehavior' in props
314+
? props.experimental_defaultFormStateBehavior
315+
: this.props.experimental_defaultFormStateBehavior;
308316
let schemaUtils: SchemaUtilsType<T, S, F> = state.schemaUtils;
309-
if (!schemaUtils || schemaUtils.doesSchemaUtilsDiffer(props.validator, rootSchema)) {
310-
schemaUtils = createSchemaUtils<T, S, F>(props.validator, rootSchema);
317+
if (
318+
!schemaUtils ||
319+
schemaUtils.doesSchemaUtilsDiffer(props.validator, rootSchema, experimental_defaultFormStateBehavior)
320+
) {
321+
schemaUtils = createSchemaUtils<T, S, F>(props.validator, rootSchema, experimental_defaultFormStateBehavior);
311322
}
312323
const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;
313324
const retrievedSchema = schemaUtils.retrieveSchema(schema, formData);

packages/core/test/Form.test.jsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,49 @@ describeRepeated('Form common', (createFormComponent) => {
13981398
});
13991399
});
14001400

1401+
describe('Default form state behavior flag', () => {
1402+
const schema = {
1403+
type: 'object',
1404+
properties: {
1405+
albums: {
1406+
type: 'array',
1407+
items: { type: 'string' },
1408+
title: 'Album Titles',
1409+
minItems: 3,
1410+
},
1411+
},
1412+
};
1413+
it('Errors when minItems is set, field is required, and minimum number of items are not present with IgnoreMinItemsUnlessRequired flag set', () => {
1414+
const { node, onError } = createFormComponent({
1415+
schema: { ...schema, required: ['albums'] },
1416+
formData: {
1417+
albums: ['Until We Have Faces'],
1418+
},
1419+
experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' },
1420+
});
1421+
submitForm(node);
1422+
sinon.assert.calledWithMatch(onError.lastCall, [
1423+
{
1424+
message: 'must NOT have fewer than 3 items',
1425+
name: 'minItems',
1426+
params: { limit: 3 },
1427+
property: '.albums',
1428+
schemaPath: '#/properties/albums/minItems',
1429+
stack: "'Album Titles' must NOT have fewer than 3 items",
1430+
},
1431+
]);
1432+
});
1433+
it('Submits when minItems is set, field is not required, and no items are present with IgnoreMinItemsUnlessRequired flag set', () => {
1434+
const { node, onSubmit } = createFormComponent({
1435+
schema,
1436+
formData: {},
1437+
experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' },
1438+
});
1439+
submitForm(node);
1440+
sinon.assert.calledWithMatch(onSubmit.lastCall, { formData: {} });
1441+
});
1442+
});
1443+
14011444
describe('Schema and external formData updates', () => {
14021445
let comp;
14031446
let onChangeProp;

packages/docs/docs/api-reference/form-props.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,41 @@ Formerly the `validate` prop.
5959
The `customValidate` prop requires a function that specifies custom validation rules for the form.
6060
See [Validation](../usage/validation.md) for more information.
6161

62+
## experimental_defaultFormStateBehavior
63+
64+
Experimental features to specify different form state behavior. Currently, this only affects the handling of optional array fields where `minItems` is set.
65+
66+
The following sub-sections represent the different keys in this object, with the tables explaining the values and their meanings.
67+
68+
### `arrayMinItems`
69+
70+
| Flag Value | Description |
71+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
72+
| `populate` | Legacy behavior - populate minItems entries with default values initially and include empty array when no values have been defined |
73+
| `requiredOnly` | Ignore `minItems` on a field when calculating defaults unless the field is required |
74+
75+
```tsx
76+
import { RJSFSchema } from '@rjsf/utils';
77+
import validator from '@rjsf/validator-ajv8';
78+
79+
const schema: RJSFSchema = {
80+
type: 'array',
81+
items: { type: 'string' },
82+
minItems: 3,
83+
};
84+
85+
render(
86+
<Form
87+
schema={schema}
88+
validator={validator}
89+
experimental_defaultFormStateBehavior={{
90+
arrayMinItems: 'requiredOnly',
91+
}}
92+
/>,
93+
document.getElementById('app')
94+
);
95+
```
96+
6297
## disabled
6398

6499
It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form.

packages/playground/src/components/Header.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,37 @@ const liveSettingsSchema: RJSFSchema = {
6565
title: 'Show Error List',
6666
enum: [false, 'top', 'bottom'],
6767
},
68+
experimental_defaultFormStateBehavior: {
69+
title: 'Default Form State Behavior (Experimental)',
70+
type: 'object',
71+
properties: {
72+
arrayMinItems: {
73+
type: 'string',
74+
title: 'minItems behavior for array field',
75+
default: 'populate',
76+
oneOf: [
77+
{
78+
type: 'string',
79+
title: 'Populate remaining minItems with default values (legacy behavior)',
80+
enum: ['populate'],
81+
},
82+
{
83+
type: 'string',
84+
title: 'Ignore minItems unless field is required',
85+
enum: ['requiredOnly'],
86+
},
87+
],
88+
},
89+
},
90+
},
91+
},
92+
};
93+
94+
const liveSettingsUiSchema: UiSchema = {
95+
experimental_defaultFormStateBehavior: {
96+
'ui:options': {
97+
label: false,
98+
},
6899
},
69100
};
70101

@@ -175,6 +206,7 @@ export default function Header({
175206
formData={liveSettings}
176207
validator={localValidator}
177208
onChange={handleSetLiveSettings}
209+
uiSchema={liveSettingsUiSchema}
178210
>
179211
<div />
180212
</Form>

packages/playground/src/components/Playground.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
4444
readonly: false,
4545
omitExtraData: false,
4646
liveOmit: false,
47+
experimental_defaultFormStateBehavior: { arrayMinItems: 'populate' },
4748
});
4849
const [FormComponent, setFormComponent] = useState<ComponentType<FormProps>>(withTheme({}));
4950
const [ArrayFieldTemplate, setArrayFieldTemplate] = useState<ComponentType<ArrayFieldTemplateProps>>();
@@ -82,6 +83,7 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
8283
setObjectFieldTemplate(ObjectFieldTemplate);
8384
setLiveSettings(liveSettings);
8485
setShowForm(true);
86+
setLiveSettings(liveSettings);
8587
},
8688
[theme, onThemeSelected, themes]
8789
);

packages/utils/src/createSchemaUtils.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import deepEquals from './deepEquals';
22
import {
33
ErrorSchema,
4+
Experimental_DefaultFormStateBehavior,
45
FormContextType,
56
GlobalUISchemaOptions,
67
IdSchema,
@@ -29,24 +30,31 @@ import {
2930
} from './schema';
3031

3132
/** The `SchemaUtils` class provides a wrapper around the publicly exported APIs in the `utils/schema` directory such
32-
* that one does not have to explicitly pass the `validator` or `rootSchema` to each method. Since both the `validator`
33-
* and `rootSchema` generally does not change across a `Form`, this allows for providing a simplified set of APIs to the
33+
* that one does not have to explicitly pass the `validator`, `rootSchema`, or `experimental_defaultFormStateBehavior` to each method.
34+
* Since these generally do not change across a `Form`, this allows for providing a simplified set of APIs to the
3435
* `@rjsf/core` components and the various themes as well. This class implements the `SchemaUtilsType` interface.
3536
*/
3637
class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
3738
implements SchemaUtilsType<T, S, F>
3839
{
3940
rootSchema: S;
4041
validator: ValidatorType<T, S, F>;
42+
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior;
4143

4244
/** Constructs the `SchemaUtils` instance with the given `validator` and `rootSchema` stored as instance variables
4345
*
4446
* @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs
4547
* @param rootSchema - The root schema that will be forwarded to all the APIs
48+
* @param experimental_defaultFormStateBehavior - Configuration flags to allow users to override default form state behavior
4649
*/
47-
constructor(validator: ValidatorType<T, S, F>, rootSchema: S) {
50+
constructor(
51+
validator: ValidatorType<T, S, F>,
52+
rootSchema: S,
53+
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior
54+
) {
4855
this.rootSchema = rootSchema;
4956
this.validator = validator;
57+
this.experimental_defaultFormStateBehavior = experimental_defaultFormStateBehavior;
5058
}
5159

5260
/** Returns the `ValidatorType` in the `SchemaUtilsType`
@@ -63,13 +71,22 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
6371
*
6472
* @param validator - An implementation of the `ValidatorType` interface that will be compared against the current one
6573
* @param rootSchema - The root schema that will be compared against the current one
74+
* @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
6675
* @returns - True if the `SchemaUtilsType` differs from the given `validator` or `rootSchema`
6776
*/
68-
doesSchemaUtilsDiffer(validator: ValidatorType<T, S, F>, rootSchema: S): boolean {
77+
doesSchemaUtilsDiffer(
78+
validator: ValidatorType<T, S, F>,
79+
rootSchema: S,
80+
experimental_defaultFormStateBehavior = {}
81+
): boolean {
6982
if (!validator || !rootSchema) {
7083
return false;
7184
}
72-
return this.validator !== validator || !deepEquals(this.rootSchema, rootSchema);
85+
return (
86+
this.validator !== validator ||
87+
!deepEquals(this.rootSchema, rootSchema) ||
88+
!deepEquals(this.experimental_defaultFormStateBehavior, experimental_defaultFormStateBehavior)
89+
);
7390
}
7491

7592
/** Returns the superset of `formData` that includes the given set updated to include any missing fields that have
@@ -87,7 +104,14 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
87104
formData?: T,
88105
includeUndefinedValues: boolean | 'excludeObjectChildren' = false
89106
): T | T[] | undefined {
90-
return getDefaultFormState<T, S, F>(this.validator, schema, formData, this.rootSchema, includeUndefinedValues);
107+
return getDefaultFormState<T, S, F>(
108+
this.validator,
109+
schema,
110+
formData,
111+
this.rootSchema,
112+
includeUndefinedValues,
113+
this.experimental_defaultFormStateBehavior
114+
);
91115
}
92116

93117
/** Determines whether the combination of `schema` and `uiSchema` properties indicates that the label for the `schema`
@@ -258,12 +282,17 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
258282
*
259283
* @param validator - an implementation of the `ValidatorType` interface that will be forwarded to all the APIs
260284
* @param rootSchema - The root schema that will be forwarded to all the APIs
285+
* @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
261286
* @returns - An implementation of a `SchemaUtilsType` interface
262287
*/
263288
export default function createSchemaUtils<
264289
T = any,
265290
S extends StrictRJSFSchema = RJSFSchema,
266291
F extends FormContextType = any
267-
>(validator: ValidatorType<T, S, F>, rootSchema: S): SchemaUtilsType<T, S, F> {
268-
return new SchemaUtils<T, S, F>(validator, rootSchema);
292+
>(
293+
validator: ValidatorType<T, S, F>,
294+
rootSchema: S,
295+
experimental_defaultFormStateBehavior = {}
296+
): SchemaUtilsType<T, S, F> {
297+
return new SchemaUtils<T, S, F>(validator, rootSchema, experimental_defaultFormStateBehavior);
269298
}

0 commit comments

Comments
 (0)