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

Add the ability to parse defaults from allOf schema #3969

Merged
merged 4 commits into from
Nov 30, 2023
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ 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.15.0

## @rjsf/utils

- Added an experimental flag `allOf` to `experimental_defaultFormStateBehavior` for populating defaults when using `allOf` schemas [#3969](https://github.com/rjsf-team/react-jsonschema-form/pull/3969)

## Dev / playground

- Added a dropdown for changing the `experimental_defaultFormStateBehavior.allOf` behaviour in the playground

# 5.14.4

## @rjsf/utils
Expand Down
62 changes: 62 additions & 0 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,68 @@ render(
);
```

### `allOf`

Optional enumerated flag controlling how empty defaults are populated when `allOf` schemas are provided, defaulting to `skipDefaults`:

| Flag Value | Description |
| ------------------ | -------------------------------------------------------------------------------------------- |
| `skipDefaults` | Skip parsing defaults from `allOf` schemas |
| `populateDefaults` | Generate default values for properties in the `allOf` schema including `if-then-else` syntax |

```tsx
import { RJSFSchema } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';

const schema: RJSFSchema = {
title: 'Example',
type: 'object',
properties: {
animalInfo: {
properties: {
animal: {
type: 'string',
default: 'Cat',
enum: ['Cat', 'Fish'],
},
},
allOf: [
{
if: {
properties: {
animal: {
const: 'Cat',
},
},
},
then: {
properties: {
food: {
type: 'string',
default: 'meat',
enum: ['meat', 'grass', 'fish'],
},
},
required: ['food'],
},
},
],
},
},
};

render(
<Form
schema={schema}
validator={validator}
experimental_defaultFormStateBehavior={{
allOf: 'populateDefaults',
}}
/>,
document.getElementById('app')
);
```

## disabled

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.
Expand Down
17 changes: 17 additions & 0 deletions packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ const liveSettingsSelectSchema: RJSFSchema = {
},
},
},
allOf: {
type: 'string',
title: 'allOf defaults behaviour',
default: 'skipDefaults',
oneOf: [
{
type: 'string',
title: 'Populate defaults with allOf',
enum: ['populateDefaults'],
},
{
type: 'string',
title: 'Skip populating defaults with allOf',
enum: ['skipDefaults'],
},
],
},
emptyObjectFields: {
type: 'string',
title: 'Object fields default behavior',
Expand Down
78 changes: 49 additions & 29 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';

import { ANY_OF_KEY, DEFAULT_KEY, DEPENDENCIES_KEY, PROPERTIES_KEY, ONE_OF_KEY, REF_KEY } from '../constants';
import {
ANY_OF_KEY,
DEFAULT_KEY,
DEPENDENCIES_KEY,
PROPERTIES_KEY,
ONE_OF_KEY,
REF_KEY,
ALL_OF_KEY,
} from '../constants';
import findSchemaDefinition from '../findSchemaDefinition';
import getClosestMatchingOption from './getClosestMatchingOption';
import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema';
Expand Down Expand Up @@ -255,41 +263,53 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
switch (getSchemaType<S>(schema)) {
// We need to recurse for object schema inner default values.
case 'object': {
const objectDefaults = Object.keys(schema.properties || {}).reduce((acc: GenericObjectType, key: string) => {
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
const computedDefault = computeDefaults<T, S, F>(validator, get(schema, [PROPERTIES_KEY, key]), {
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
includeUndefinedValues: includeUndefinedValues === true,
parentDefaults: get(defaults, [key]),
rawFormData: get(formData, [key]),
required: schema.required?.includes(key),
});
maybeAddDefaultToObject<T>(
acc,
key,
computedDefault,
includeUndefinedValues,
required,
schema.required,
experimental_defaultFormStateBehavior
);
return acc;
}, {}) as T;
if (schema.additionalProperties) {
// This is a custom addition that fixes this issue:
// https://github.com/rjsf-team/react-jsonschema-form/issues/3832
const retrievedSchema =
experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData)
: schema;
const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget if it is possible for allOf to be in something other than the object type?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjdlambert Do you know whether this is possible? I can imagine if it is, then this solution may be incomplete

Copy link
Contributor Author

@benjdlambert benjdlambert Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so I guess you mean something like this right?

const schema = { anyOf: [{ type: 'string' }, { type: 'number' }] }

Or do you have an example schema in mind that I can test with?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an anyOf. I'm guessing that you probably haven't seen an JSON Schema that has an allOf that isn't part of an object type?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, this is better than what we had before, so I'm going to approve

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, actually i'm not sure. I've never seen one, not sure if there's any other datatypes where it would be possible to use allOf like that. Happy to provide a followup if there are!

(acc: GenericObjectType, key: string) => {
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
const computedDefault = computeDefaults<T, S, F>(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), {
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
includeUndefinedValues: includeUndefinedValues === true,
parentDefaults: get(defaults, [key]),
rawFormData: get(formData, [key]),
required: retrievedSchema.required?.includes(key),
});
maybeAddDefaultToObject<T>(
acc,
key,
computedDefault,
includeUndefinedValues,
required,
retrievedSchema.required,
experimental_defaultFormStateBehavior
);
return acc;
},
{}
) as T;
if (retrievedSchema.additionalProperties) {
// as per spec additionalProperties may be either schema or boolean
const additionalPropertiesSchema = isObject(schema.additionalProperties) ? schema.additionalProperties : {};
const additionalPropertiesSchema = isObject(retrievedSchema.additionalProperties)
? retrievedSchema.additionalProperties
: {};

const keys = new Set<string>();
if (isObject(defaults)) {
Object.keys(defaults as GenericObjectType)
.filter((key) => !schema.properties || !schema.properties[key])
.filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key])
.forEach((key) => keys.add(key));
}
const formDataRequired: string[] = [];
Object.keys(formData as GenericObjectType)
.filter((key) => !schema.properties || !schema.properties[key])
.filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key])
.forEach((key) => {
keys.add(key);
formDataRequired.push(key);
Expand All @@ -302,7 +322,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
includeUndefinedValues: includeUndefinedValues === true,
parentDefaults: get(defaults, [key]),
rawFormData: get(formData, [key]),
required: schema.required?.includes(key),
required: retrievedSchema.required?.includes(key),
});
// Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
maybeAddDefaultToObject<T>(
Expand Down
4 changes: 4 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export type Experimental_DefaultFormStateBehavior = {
* - `skipDefaults`: Does not set defaults |
*/
emptyObjectFields?: 'populateAllDefaults' | 'populateRequiredDefaults' | 'skipDefaults';
/**
* Optional flag to compute the default form state using allOf and if/then/else schemas. Defaults to `skipDefaults'.
*/
allOf?: 'populateDefaults' | 'skipDefaults';
};

/** The interface representing a Date object that contains an optional time */
Expand Down
95 changes: 95 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,101 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
).toEqual({ requiredArray: ['default0', 'default0'] });
});
});
describe('default form state behaviour: allOf = "populateDefaults"', () => {
it('should populate default values correctly', () => {
const schema: RJSFSchema = {
title: 'Example',
type: 'object',
properties: {
animalInfo: {
properties: {
animal: {
type: 'string',
default: 'Cat',
enum: ['Cat', 'Fish'],
},
},
allOf: [
{
if: {
properties: {
animal: {
const: 'Cat',
},
},
},
then: {
properties: {
food: {
type: 'string',
default: 'meat',
enum: ['meat', 'grass', 'fish'],
},
},
required: ['food'],
},
},
],
},
},
};

expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { allOf: 'populateDefaults' },
})
).toEqual({ animalInfo: { animal: 'Cat', food: 'meat' } });
});
});

describe('default form state behaviour: allOf = "skipDefaults"', () => {
it('should populate default values correctly', () => {
const schema: RJSFSchema = {
title: 'Example',
type: 'object',
properties: {
animalInfo: {
properties: {
animal: {
type: 'string',
default: 'Cat',
enum: ['Cat', 'Fish'],
},
},
allOf: [
{
if: {
properties: {
animal: {
const: 'Cat',
},
},
},
then: {
properties: {
food: {
type: 'string',
default: 'meat',
enum: ['meat', 'grass', 'fish'],
},
},
required: ['food'],
},
},
],
},
},
};

expect(
computeDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { allOf: 'skipDefaults' },
})
).toEqual({ animalInfo: { animal: 'Cat' } });
});
});
describe('default form state behavior: arrayMinItems.populate = "never"', () => {
it('should not be filled if minItems defined and required', () => {
const schema: RJSFSchema = {
Expand Down