Skip to content
Draft
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 7.0.0

## @rjsf/utils

- Extended `ui:emptyValue` to apply whenever a field is blank (initial render, reset, or user clearing), not just on widget clear ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983))
- Added `ui:initialValue` to pre-fill fields on render/reset, taking priority over `schema.default` ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983))
- Added `ui:required` to override schema required status from uiSchema ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983))

## @rjsf/core

- Updated `Form` to pass `uiSchema` through `getDefaultFormState` for `ui:emptyValue` and `ui:initialValue` support ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983))
- Updated `SchemaField` and `LayoutGridField` to respect `ui:required` override ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983))

## Dev / docs / playground

- Added documentation and playground examples for `ui:emptyValue` (extended), `ui:initialValue`, and `ui:required` ([#4983](https://github.com/rjsf-team/react-jsonschema-form/issues/4983))

# 6.4.2

## @rjsf/core
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, ElementType, FormEvent, ReactNode, Ref, RefObject, createRef } from 'react';
import {
augmentSchemaWithUiRequired,
createSchemaUtils,
CustomValidator,
deepEquals,
Expand Down Expand Up @@ -592,6 +593,7 @@ export default class Form<
defaultsFormData,
false,
state.initialDefaultsGenerated,
uiSchema,
) as T;
const _retrievedSchema = this.updateRetrievedSchema(
retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData),
Expand Down Expand Up @@ -719,9 +721,10 @@ export default class Form<
const schemaUtils = altSchemaUtils ? altSchemaUtils : this.state.schemaUtils;
const { customValidate, transformErrors, uiSchema } = this.props;
const resolvedSchema = retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData);
const effectiveSchema = augmentSchemaWithUiRequired<T, S>(resolvedSchema, uiSchema);
return schemaUtils
.getValidator()
.validateFormData(formData, resolvedSchema, customValidate, transformErrors, uiSchema);
.validateFormData(formData, effectiveSchema, customValidate, transformErrors, uiSchema);
}

/** Renders any errors contained in the `state` in using the `ErrorList`, if not disabled by `showErrorList`. */
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/components/fields/LayoutGridField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,8 @@ function LayoutGridFieldComponent<T = any, S extends StrictRJSFSchema = RJSFSche
// the `Form` via the prop passed to `LayoutGridField` we need to make sure the uiSchema always has a true value
// when it is needed
const { fieldUiSchema, uiReadonly } = computeFieldUiSchema<T, S, F>(name, uiProps, uiSchema, isReadonly, readonly);
const fieldUiOptions = getUiOptions<T, S, F>(fieldUiSchema);
const effectiveRequired = fieldUiOptions.required !== undefined ? Boolean(fieldUiOptions.required) : isRequired;

return (
<Field
Expand All @@ -691,7 +693,7 @@ function LayoutGridFieldComponent<T = any, S extends StrictRJSFSchema = RJSFSche
}
{...otherProps}
name={name}
required={isRequired}
required={effectiveRequired}
readonly={uiReadonly}
schema={schema}
uiSchema={fieldUiSchema}
Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/components/fields/SchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
const FieldComponent = getFieldComponent<T, S, F>(schema, uiOptions, registry);
const disabled = Boolean(uiOptions.disabled ?? props.disabled);
const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly));
const effectiveRequired = uiOptions.required !== undefined ? Boolean(uiOptions.required) : required;
if (
uiOptions.required === false &&
required &&
uiOptions.initialValue === undefined &&
uiOptions.emptyValue === undefined
) {
console.warn(
`ui:required is false for a schema-required field "${name}" but no ui:initialValue or ui:emptyValue is set. ` +
'The UI will show this field as optional but schema validation will still fail if left empty.',
);
}
const uiSchemaHideError = uiOptions.hideError;
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError);
Expand Down Expand Up @@ -167,7 +179,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
);
}
// When the anyOf/oneOf is an optional data control render AND it does not have form data, hide the label
const isOptionalRender = shouldRenderOptionalField<T, S, F>(registry, schema, required, uiSchema);
const isOptionalRender = shouldRenderOptionalField<T, S, F>(registry, schema, effectiveRequired, uiSchema);
const hasFormData = isFormDataAvailable<T>(formData);
displayLabel = displayLabel && (!isOptionalRender || hasFormData);
fieldPathIdProps = {
Expand Down Expand Up @@ -274,7 +286,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
onKeyRename,
onKeyRenameBlur,
onRemoveProperty,
required,
required: effectiveRequired,
disabled,
readonly,
hideError,
Expand Down
96 changes: 96 additions & 0 deletions packages/core/test/uiSchema.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2865,4 +2865,100 @@ describe('uiSchema', () => {
expect(node.querySelectorAll("input[placeholder='Node name']")).toHaveLength(5);
});
});

describe('ui:required', () => {
it('shows required asterisk on non-required field when ui:required is true', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
foo: { type: 'string' },
},
};
const uiSchema: UiSchema = {
foo: { 'ui:required': true },
};
const { node } = createFormComponent({ schema, uiSchema });
expect(node.querySelector('.rjsf-field-string label span.required')).not.toBeNull();
});

it('hides required asterisk on schema-required field when ui:required is false', () => {
const schema: RJSFSchema = {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
},
};
const uiSchema: UiSchema = {
foo: { 'ui:required': false, 'ui:initialValue': 'fallback' },
};
const { node } = createFormComponent({ schema, uiSchema });
expect(node.querySelector('.rjsf-field-string label span.required')).toBeNull();
});

it('produces validation error when ui:required is true and field is empty', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
foo: { type: 'string' },
},
};
const uiSchema: UiSchema = {
foo: { 'ui:required': true },
};
const { node, onSubmit } = createFormComponent({ schema, uiSchema });
submitForm(node);
expect(onSubmit).not.toHaveBeenCalled();
});

it('does not suppress schema validation when ui:required is false', () => {
const schema: RJSFSchema = {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
},
};
const uiSchema: UiSchema = {
foo: { 'ui:required': false },
};
const { node, onSubmit } = createFormComponent({ schema, uiSchema });
submitForm(node);
expect(onSubmit).not.toHaveBeenCalled();
});

it('emits console.warn for ui:required false without initialValue or emptyValue', () => {
const schema: RJSFSchema = {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
},
};
const uiSchema: UiSchema = {
foo: { 'ui:required': false },
};
createFormComponent({ schema, uiSchema });
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('ui:required is false for a schema-required field'),
);
});

it('does not emit console.warn for ui:required false with initialValue set', () => {
const schema: RJSFSchema = {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
},
};
const uiSchema: UiSchema = {
foo: { 'ui:required': false, 'ui:initialValue': 'x' },
};
createFormComponent({ schema, uiSchema });
expect(consoleWarnSpy).not.toHaveBeenCalledWith(
expect.stringContaining('ui:required is false for a schema-required field'),
);
});
});
});
46 changes: 45 additions & 1 deletion packages/docs/docs/api-reference/uiSchema.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,25 @@ render(

### emptyValue

The `ui:emptyValue` uiSchema directive provides the default value to use when an input for a field is empty
The `ui:emptyValue` uiSchema directive provides the default value to use when a field is empty. This applies whenever the field is blank, whether from initial render, a form reset, or the user clearing the input.

```tsx
import { RJSFSchema, UiSchema } from '@rjsf/utils';

const schema: RJSFSchema = {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
},
};

const uiSchema: UiSchema = {
name: {
'ui:emptyValue': '',
},
};
```

### enumDisabled

Expand Down Expand Up @@ -588,6 +606,28 @@ If you need to enable the default error display of a child in the hierarchy afte

This is useful when you have a custom field or widget that utilizes either the `rawErrors` or the `errorSchema` to manipulate and/or show the error(s) for the field/widget itself.

### initialValue

The `ui:initialValue` uiSchema directive pre-fills a field on initial render and after a form reset. It takes priority over `schema.default` but does not override user-provided `formData`. Useful for hidden fields that need a fixed value.

```tsx
import { RJSFSchema, UiSchema } from '@rjsf/utils';

const schema: RJSFSchema = {
type: 'object',
properties: {
country: { type: 'string' },
},
};

const uiSchema: UiSchema = {
country: {
'ui:initialValue': 'US',
'ui:widget': 'hidden',
},
};
```

### inputType

To change the input type (for example, `tel` or `email`) you can specify the `inputType` in the `ui:options` uiSchema directive.
Expand Down Expand Up @@ -682,6 +722,10 @@ The `ui:readonly` uiSchema directive will mark all child widgets from a given fi

> Note: If you're wondering about the difference between a `disabled` field and a `readonly` one: Marking a field as read-only will render it greyed out, but its text value will be selectable. Disabling it will prevent its value to be selected at all.

### required

The `ui:required` uiSchema directive overrides the schema's `required` status for a field. Setting `ui:required` to `true` shows the required indicator and produces a validation error if the field is left empty. Setting it to `false` hides the required indicator, but does not suppress schema-level validation. A `console.warn` is emitted when `ui:required` is `false` on a schema-required field without `ui:initialValue` or `ui:emptyValue` set.

### rows

You can set the initial height of a textarea widget by specifying `rows` option.
Expand Down
16 changes: 16 additions & 0 deletions packages/playground/src/samples/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const optionsSample: Sample = {
title: 'Telephone',
minLength: 10,
},
country: {
type: 'string',
title: 'Country',
},
email: {
type: 'string',
title: 'Email',
},
},
},
uiSchema: {
Expand All @@ -41,6 +49,7 @@ const optionsSample: Sample = {
'ui:title': 'Surname',
'ui:emptyValue': '',
'ui:autocomplete': 'given-name',
'ui:required': false,
},
age: {
'ui:widget': 'updown',
Expand All @@ -62,6 +71,13 @@ const optionsSample: Sample = {
inputType: 'tel',
},
},
country: {
'ui:initialValue': 'US',
'ui:widget': 'hidden',
},
email: {
'ui:required': true,
},
},
formData: {
lastName: 'Norris',
Expand Down
59 changes: 59 additions & 0 deletions packages/utils/src/augmentSchemaWithUiRequired.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import get from 'lodash/get';

import getUiOptions from './getUiOptions';
import { FormContextType, RJSFSchema, StrictRJSFSchema, UiSchema } from './types';

/** Recursively walks a schema and uiSchema, adding fields marked `ui:required: true` to the schema's `required`
* arrays. Returns a new schema without mutating the original.
*
* @param schema - The schema to augment
* @param [uiSchema] - The uiSchema containing ui:required overrides
* @returns A new schema with ui:required fields added to required arrays
*/
export default function augmentSchemaWithUiRequired<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(schema: S, uiSchema?: UiSchema<T, S, F>): S {
if (!uiSchema || schema.type !== 'object' || !schema.properties) {
return schema;
}

const existingRequired = schema.required || [];
const additionalRequired: string[] = [];
let propertiesChanged = false;
const newProperties: Record<string, unknown> = {};

for (const key of Object.keys(schema.properties)) {
const fieldUiSchema = get(uiSchema, [key]);
const propertySchema = schema.properties[key] as S;

if (fieldUiSchema) {
const { required: uiRequired } = getUiOptions<T, S, F>(fieldUiSchema);
if (uiRequired === true && !existingRequired.includes(key)) {
additionalRequired.push(key);
}
}

// Recurse into nested objects
if (propertySchema && propertySchema.type === 'object' && fieldUiSchema) {
const augmented = augmentSchemaWithUiRequired<T, S, F>(propertySchema, fieldUiSchema);
if (augmented !== propertySchema) {
propertiesChanged = true;
newProperties[key] = augmented;
continue;
}
}
newProperties[key] = propertySchema;
}

if (additionalRequired.length === 0 && !propertiesChanged) {
return schema;
}

return {
...schema,
...(propertiesChanged ? { properties: newProperties } : {}),
...(additionalRequired.length > 0 ? { required: [...existingRequired, ...additionalRequired] } : {}),
};
}
3 changes: 3 additions & 0 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,15 @@ class SchemaUtils<
* If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested
* object properties.
* @param initialDefaultsGenerated - Indicates whether or not initial defaults have been generated
* @param [uiSchema] - Optional uiSchema, used to apply ui:emptyValue and ui:initialValue as defaults
* @returns - The resulting `formData` with all the defaults provided
*/
getDefaultFormState(
schema: S,
formData?: T,
includeUndefinedValues: boolean | 'excludeObjectChildren' = false,
initialDefaultsGenerated?: boolean,
uiSchema?: UiSchema<T, S, F>,
): T | T[] | undefined {
return getDefaultFormState<T, S, F>(
this.validator,
Expand All @@ -186,6 +188,7 @@ class SchemaUtils<
this.experimental_defaultFormStateBehavior,
this.experimental_customMergeAllOf,
initialDefaultsGenerated,
uiSchema,
);
}

Expand Down
Loading
Loading