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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/core

- Fixed duplicate label and description rendering in `CheckboxWidget` by conditionally rendering them based on widget type
- Updated `CheckboxWidget` to handle label and description rendering consistently
- Modified `FieldTemplate` to skip label and description rendering for checkbox widgets, fixing ([#4742](https://github.com/rjsf-team/react-jsonschema-form/issues/4742))
- Updated `CheckboxWidget` to handle label and description rendering consistently
- Modified `FieldTemplate` to skip label and description rendering for checkbox widgets, fixing ([#4742](https://github.com/rjsf-team/react-jsonschema-form/issues/4742))
- Updated `ObjectField` to change the removal of an additional property to defer the work to the `processPendingChange()` handler in `Form`, fixing [#4850](https://github.com/rjsf-team/react-jsonschema-form/issues/4850)
- Updated `FallbackField` to support `object` and `array` types, and improved `ArrayField` so that it handles missing items properly with the fallback field

## @rjsf/fluentui-rc

Expand Down Expand Up @@ -66,7 +67,6 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/utils

- Updated `getDefaultFormState()` to not save an undefined field value into an object when the type is `null` and `excludeObjectChildren` is provided, fixing [#4821](https://github.com/rjsf-team/react-jsonschema-form/issues/4821)
- Added new `ADDITIONAL_PROPERTY_KEY_REMOVE` constant

## Dev / docs / playground

Expand Down
5 changes: 1 addition & 4 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, ElementType, FormEvent, ReactNode, Ref, RefObject, createRef } from 'react';
import {
ADDITIONAL_PROPERTY_KEY_REMOVE,
createSchemaUtils,
CustomValidator,
deepEquals,
Expand Down Expand Up @@ -55,9 +54,7 @@ import _toPath from 'lodash/toPath';
import _unset from 'lodash/unset';

import getDefaultRegistry from '../getDefaultRegistry';

/** Internal only symbol used by the `reset()` function to indicate that a reset operation is happening */
const IS_RESET = Symbol('reset');
import { ADDITIONAL_PROPERTY_KEY_REMOVE, IS_RESET } from './constants';

/** The properties that are passed to the `Form` */
export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/components/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Internal only symbol used by `Form` & `ObjectField` to mark an additional property element to be removed */
export const ADDITIONAL_PROPERTY_KEY_REMOVE = Symbol('remove-this-key');

/** Internal only symbol used by the `reset()` function to indicate that a reset operation is happening */
export const IS_RESET = Symbol('reset');
74 changes: 45 additions & 29 deletions packages/core/src/components/fields/ArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,12 @@ function getNewFormDataRow<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
registry: Registry<T[], S, F>,
schema: S,
): T {
const { schemaUtils } = registry;
const { schemaUtils, globalFormOptions } = registry;
let itemSchema = schema.items as S;
if (isFixedItems(schema) && allowAdditionalItems(schema)) {
if (globalFormOptions.useFallbackUiForUnsupportedType && !itemSchema) {
// If we don't have itemSchema and useFallbackUiForUnsupportedType is on, use an empty schema
itemSchema = {} as S;
} else if (isFixedItems(schema) && allowAdditionalItems(schema)) {
itemSchema = schema.additionalItems as S;
}
// Cast this as a T to work around schema utils being for T[] caused by the FieldProps<T[], S, F> call on the class
Expand Down Expand Up @@ -840,7 +843,7 @@ export default function ArrayField<T = any, S extends StrictRJSFSchema = RJSFSch
props: FieldProps<T[], S, F>,
) {
const { schema, uiSchema, errorSchema, fieldPathId, registry, formData, onChange } = props;
const { schemaUtils, translateString } = registry;
const { globalFormOptions, schemaUtils, translateString } = registry;
const { keyedFormData, updateKeyedFormData } = useKeyedFormData<T>(formData);
// All the children will use childFieldPathId if present in the props, falling back to the fieldPathId
const childFieldPathId = props.childFieldPathId ?? fieldPathId;
Expand Down Expand Up @@ -1027,43 +1030,56 @@ export default function ArrayField<T = any, S extends StrictRJSFSchema = RJSFSch
[onChange, childFieldPathId],
);

if (!(ITEMS_KEY in schema)) {
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
const UnsupportedFieldTemplate = getTemplate<'UnsupportedFieldTemplate', T[], S, F>(
'UnsupportedFieldTemplate',
registry,
uiOptions,
);

return (
<UnsupportedFieldTemplate
schema={schema}
fieldPathId={fieldPathId}
reason={translateString(TranslatableString.MissingItems)}
registry={registry}
/>
);
}
const arrayProps = {
const arrayAsMultiProps: ArrayAsFieldProps<T[], S, F> = {
...props,
formData,
fieldPathId: childFieldPathId,
onSelectChange: onSelectChange,
};
const arrayProps: InternalArrayFieldProps<T, S, F> = {
...props,
handleAddItem,
handleCopyItem,
handleRemoveItem,
handleReorderItems,
keyedFormData,
onChange: handleChange,
};
if (schemaUtils.isMultiSelect(schema)) {
if (!(ITEMS_KEY in schema)) {
if (!globalFormOptions.useFallbackUiForUnsupportedType) {
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
const UnsupportedFieldTemplate = getTemplate<'UnsupportedFieldTemplate', T[], S, F>(
'UnsupportedFieldTemplate',
registry,
uiOptions,
);

return (
<UnsupportedFieldTemplate
schema={schema}
fieldPathId={fieldPathId}
reason={translateString(TranslatableString.MissingItems)}
registry={registry}
/>
);
}
// Add an items schema with type as undefined so it triggers FallbackField later on
const fallbackSchema = { ...schema, [ITEMS_KEY]: { type: undefined } };
arrayAsMultiProps.schema = fallbackSchema;
arrayProps.schema = fallbackSchema;
}
if (schemaUtils.isMultiSelect(arrayAsMultiProps.schema)) {
// If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
return <ArrayAsMultiSelect<T, S, F> {...props} fieldPathId={childFieldPathId} onSelectChange={onSelectChange} />;
return <ArrayAsMultiSelect<T, S, F> {...arrayAsMultiProps} />;
}
if (isCustomWidget<T[], S, F>(uiSchema)) {
return <ArrayAsCustomWidget<T, S, F> {...props} fieldPathId={childFieldPathId} onSelectChange={onSelectChange} />;
return <ArrayAsCustomWidget<T, S, F> {...arrayAsMultiProps} />;
}
if (isFixedItems(schema)) {
return <FixedArray<T, S, F> {...props} {...arrayProps} />;
if (isFixedItems(arrayAsMultiProps.schema)) {
return <FixedArray<T, S, F> {...arrayProps} />;
}
if (schemaUtils.isFilesArray(schema, uiSchema)) {
return <ArrayAsFiles {...props} fieldPathId={childFieldPathId} onSelectChange={onSelectChange} />;
if (schemaUtils.isFilesArray(arrayAsMultiProps.schema, uiSchema)) {
return <ArrayAsFiles<T, S, F> {...arrayAsMultiProps} />;
}
return <NormalArray<T, S, F> {...props} {...arrayProps} />;
return <NormalArray<T, S, F> {...arrayProps} />;
}
28 changes: 18 additions & 10 deletions packages/core/src/components/fields/FallbackField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { JSONSchema7TypeName } from 'json-schema';
function getFallbackTypeSelectionSchema(title: string): RJSFSchema {
return {
type: 'string',
enum: ['string', 'number', 'boolean'],
enum: ['string', 'number', 'boolean', 'object', 'array'],
default: 'string',
title: title,
};
Expand All @@ -36,6 +36,9 @@ function getTypeOfFormData(formData: any): JSONSchema7TypeName {
if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') {
return dataType;
}
if (dataType === 'object') {
return Array.isArray(formData) ? 'array' : 'object';
}
// Treat everything else as a string
return 'string';
}
Expand Down Expand Up @@ -106,20 +109,14 @@ export default function FallbackField<
};

if (!globalFormOptions.useFallbackUiForUnsupportedType) {
const { reason = translateString(TranslatableString.UnknownFieldType, [String(schema.type)]) } = props;
const UnsupportedFieldTemplate = getTemplate<'UnsupportedFieldTemplate', T, S, F>(
'UnsupportedFieldTemplate',
registry,
uiOptions,
);

return (
<UnsupportedFieldTemplate
schema={schema}
fieldPathId={fieldPathId}
reason={translateString(TranslatableString.UnknownFieldType, [String(schema.type)])}
registry={registry}
/>
);
return <UnsupportedFieldTemplate schema={schema} fieldPathId={fieldPathId} reason={reason} registry={registry} />;
}

const FallbackFieldTemplate = getTemplate<'FallbackFieldTemplate', T, S, F>(
Expand Down Expand Up @@ -151,7 +148,18 @@ export default function FallbackField<
required={required}
/>
}
schemaField={<SchemaField {...props} schema={{ type, title: translateString(TranslatableString.Value) } as S} />}
schemaField={
<SchemaField
{...props}
schema={
{
type,
title: translateString(TranslatableString.Value),
...(type === 'object' && { additionalProperties: true }),
} as S
}
/>
}
/>
);
}
3 changes: 2 additions & 1 deletion packages/core/src/components/fields/ObjectField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FocusEvent, useCallback, useState } from 'react';
import {
ADDITIONAL_PROPERTY_KEY_REMOVE,
ADDITIONAL_PROPERTY_FLAG,
ANY_OF_KEY,
getTemplate,
Expand Down Expand Up @@ -30,6 +29,8 @@ import has from 'lodash/has';
import isObject from 'lodash/isObject';
import set from 'lodash/set';

import { ADDITIONAL_PROPERTY_KEY_REMOVE } from '../constants';

/** Returns a flag indicating whether the `name` field is required in the object schema
*
* @param schema - The schema to check
Expand Down
63 changes: 63 additions & 0 deletions packages/core/test/Form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,69 @@ describeRepeated('Form common', (createFormComponent) => {
},
'root_unknownProperty',
);

// Change the fallback type to 'boolean'
fireEvent.change(node.querySelector('select'), { target: { value: 2 } });
optionValue = node.querySelector('select').value;
optionText = node.querySelector(`select option[value="${optionValue}"]`).textContent;
expect(optionText).to.equal('boolean');
expect(node.querySelector('input[type=checkbox]')).to.exist;
expect(node.querySelector('input[type=checkbox]').checked).to.equal(true);
// Verify formData was casted to number
sinon.assert.calledWithMatch(
onChange.lastCall,
{
formData: {
unknownProperty: true,
},
schema,
},
'root_unknownProperty',
);

// Change the fallback type to 'object'
fireEvent.change(node.querySelector('select'), { target: { value: 3 } });
optionValue = node.querySelector('select').value;
optionText = node.querySelector(`select option[value="${optionValue}"]`).textContent;
expect(optionText).to.equal('object');
let addButton = node.querySelector('.rjsf-object-property-expand button');
expect(addButton).to.exist;
// click the add button
act(() => addButton.click());

// Verify formData was casted to number
sinon.assert.calledWithMatch(
onChange.lastCall,
{
formData: {
unknownProperty: { newKey: 'New Value' },
},
schema,
},
'root_unknownProperty',
);

// Change the fallback type to 'array'
fireEvent.change(node.querySelector('select'), { target: { value: 4 } });
optionValue = node.querySelector('select').value;
optionText = node.querySelector(`select option[value="${optionValue}"]`).textContent;
expect(optionText).to.equal('array');
addButton = node.querySelector('.rjsf-array-item-add button');
expect(addButton).to.exist;
// click the add button
act(() => addButton.click());

// Verify formData was casted to number
sinon.assert.calledWithMatch(
onChange.lastCall,
{
formData: {
unknownProperty: [undefined],
},
schema,
},
'root_unknownProperty',
);
});
});

Expand Down
1 change: 0 additions & 1 deletion packages/utils/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* utility.
*/
export const ADDITIONAL_PROPERTY_FLAG = '__additional_property';
export const ADDITIONAL_PROPERTY_KEY_REMOVE = Symbol('remove-this-key');
Copy link
Member Author

@heath-freenome heath-freenome Nov 6, 2025

Choose a reason for hiding this comment

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

I did this so that if someone just updated core they wouldn't have an issue with a missing constant

export const ADDITIONAL_PROPERTIES_KEY = 'additionalProperties';
export const ALL_OF_KEY = 'allOf';
export const ANY_OF_KEY = 'anyOf';
Expand Down