diff --git a/CHANGELOG.md b/CHANGELOG.md index 430724c2d5..c82d3b3f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ 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.23.0 + +## @rjsf/utils + +- Updated `getTemplate()` to allow per-field customization using string key from `Registry`, fixing [#3695](https://github.com/rjsf-team/react-jsonschema-form/issues/3695). +- Updated `TemplatesType` to allow for a string key to be used to reference a custom template in the `Registry`, fixing [#3695](https://github.com/rjsf-team/react-jsonschema-form/issues/3695) +- Updated tests to cover the new `getTemplate()` functionality + +## Dev / docs / playground + +- Updated `advanced-customization/custom-templates` with the new feature. # 5.22.2 diff --git a/packages/docs/docs/advanced-customization/custom-templates.md b/packages/docs/docs/advanced-customization/custom-templates.md index 6491efbb5c..7872050c57 100644 --- a/packages/docs/docs/advanced-customization/custom-templates.md +++ b/packages/docs/docs/advanced-customization/custom-templates.md @@ -76,16 +76,27 @@ render( ); ``` -You also can provide your own field template to a uiSchema by specifying a `ui:ArrayFieldTemplate` property. +You also can provide your own field template to a uiSchema by specifying a `ui:ArrayFieldTemplate` property with your Component : ```tsx import { UiSchema } from '@rjsf/utils'; +import ArrayFieldTemplate from './ArrayFieldTemplate'; const uiSchema: UiSchema = { 'ui:ArrayFieldTemplate': ArrayFieldTemplate, }; ``` +or a string value from the `Registry` : + +```tsx +import { UiSchema } from '@rjsf/utils'; + +const uiSchema: UiSchema = { + 'ui:ArrayFieldTemplate': 'CustomArrayFieldTemplate', +}; +``` + Please see the [customArray.tsx sample](https://github.com/rjsf-team/react-jsonschema-form/blob/main/packages/playground/src/samples/customArray.tsx) from the [playground](https://rjsf-team.github.io/react-jsonschema-form/) for another example. The following props are passed to each `ArrayFieldTemplate`: @@ -163,16 +174,27 @@ render( ); ``` -You also can provide your own template to a uiSchema by specifying a `ui:ArrayFieldDescriptionTemplate` property. +You also can provide your own field template to a uiSchema by specifying a `ui:ArrayFieldDescriptionTemplate` property with your Component : ```tsx import { UiSchema } from '@rjsf/utils'; +import ArrayFieldDescriptionTemplate from './ArrayFieldDescriptionTemplate'; const uiSchema: UiSchema = { 'ui:ArrayFieldDescriptionTemplate': ArrayFieldDescriptionTemplate, }; ``` +or a string value from the `Registry` : + +```tsx +import { UiSchema } from '@rjsf/utils'; + +const uiSchema: UiSchema = { + 'ui:ArrayFieldDescriptionTemplate': 'CustomArrayFieldDescriptionTemplate', +}; +``` + The following props are passed to each `ArrayFieldDescriptionTemplate`: - `description`: The description of the array field being rendered. @@ -261,13 +283,24 @@ render( ); ``` -You also can provide your own template to a uiSchema by specifying a `ui:ArrayFieldDescriptionTemplate` property. +You also can provide your own template to a uiSchema by specifying a `ui:ArrayFieldDescriptionTemplate` property with your Component : ```tsx import { UiSchema } from '@rjsf/utils'; +import ArrayFieldDescriptionTemplate from './ArrayFieldDescriptionTemplate'; const uiSchema: UiSchema = { - 'ui:ArrayFieldTitleTemplate': ArrayFieldTitleTemplate, + 'ui:ArrayFieldDescriptionTemplate': ArrayFieldDescriptionTemplate, +}; +``` + +or a string value from the `Registry` : + +```tsx +import { UiSchema } from '@rjsf/utils'; + +const uiSchema: UiSchema = { + 'ui:ArrayFieldDescriptionTemplate': 'CustomArrayFieldDescriptionTemplate', }; ``` @@ -615,16 +648,27 @@ render( ); ``` -You also can provide your own field template to a uiSchema by specifying a `ui:FieldTemplate` property. +You also can provide your own field template to a uiSchema by specifying a `ui:FieldTemplate` property with your Component : ```tsx import { UiSchema } from '@rjsf/utils'; +import CustomFieldTemplate from './CustomFieldTemplate'; const uiSchema: UiSchema = { 'ui:FieldTemplate': CustomFieldTemplate, }; ``` +or a string value from the `Registry` : + +```tsx +import { UiSchema } from '@rjsf/utils'; + +const uiSchema: UiSchema = { + 'ui:FieldTemplate': 'CustomFieldTemplate', +}; +``` + If you want to handle the rendering of each element yourself, you can use the props `rawHelp`, `rawDescription` and `rawErrors`. The following props are passed to a custom field template component: @@ -693,16 +737,27 @@ render( ); ``` -You also can provide your own field template to a uiSchema by specifying a `ui:ObjectFieldTemplate` property. +You also can provide your own field template to a uiSchema by specifying a `ui:ObjectFieldTemplate` property with your Component : ```tsx import { UiSchema } from '@rjsf/utils'; +import ObjectFieldTemplate from './ObjectFieldTemplate'; const uiSchema: UiSchema = { 'ui:ObjectFieldTemplate': ObjectFieldTemplate, }; ``` +or a string value from the `Registry` : + +```tsx +import { UiSchema } from '@rjsf/utils'; + +const uiSchema: UiSchema = { + 'ui:ObjectFieldTemplate': 'ObjectFieldTemplate', +}; +``` + Please see the [customObject.tsx sample](https://github.com/rjsf-team/react-jsonschema-form/blob/main/packages/playground/src/samples/customObject.tsx) from the [playground](https://rjsf-team.github.io/react-jsonschema-form/) for a better example. The following props are passed to each `ObjectFieldTemplate` as defined by the `ObjectFieldTemplateProps` in `@rjsf/utils`: diff --git a/packages/utils/src/getTemplate.ts b/packages/utils/src/getTemplate.ts index 8863df65d5..ca9733d53f 100644 --- a/packages/utils/src/getTemplate.ts +++ b/packages/utils/src/getTemplate.ts @@ -18,6 +18,17 @@ export default function getTemplate< if (name === 'ButtonTemplates') { return templates[name]; } + // Allow templates to be customized per-field by using string keys from the registry + if ( + Object.hasOwn(uiOptions, name) && + typeof uiOptions[name] === 'string' && + Object.hasOwn(templates, uiOptions[name] as string) + ) { + const key = uiOptions[name]; + // Evaluating templates[key] results in TS2590: Expression produces a union type that is too complex to represent + // To avoid that, we cast templates to `any` before accessing the key field + return (templates as any)[key]; + } return ( // Evaluating uiOptions[name] results in TS2590: Expression produces a union type that is too complex to represent // To avoid that, we cast uiOptions to `any` before accessing the name field diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 0b6c400504..ae3a735377 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -223,51 +223,45 @@ export type FormValidation = FieldValidation & { }; /** The properties that are passed to an `ErrorListTemplate` implementation */ -export type ErrorListProps = { +export type ErrorListProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The errorSchema constructed by `Form` */ errorSchema: ErrorSchema; /** An array of the errors */ errors: RJSFValidationError[]; /** The `formContext` object that was passed to `Form` */ formContext?: F; - /** The schema that was passed to `Form` */ - schema: S; - /** The uiSchema that was passed to `Form` */ - uiSchema?: UiSchema; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to an `FieldErrorTemplate` implementation */ -export type FieldErrorProps = { +export type FieldErrorProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The errorSchema constructed by `Form` */ errorSchema?: ErrorSchema; /** An array of the errors */ errors?: Array; /** The tree of unique ids for every child field */ idSchema: IdSchema; - /** The schema that was passed to field */ - schema: S; - /** The uiSchema that was passed to field */ - uiSchema?: UiSchema; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to an `FieldHelpTemplate` implementation */ -export type FieldHelpProps = { +export type FieldHelpProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The help information to be rendered */ help?: string | ReactElement; /** The tree of unique ids for every child field */ idSchema: IdSchema; - /** The schema that was passed to field */ - schema: S; - /** The uiSchema that was passed to field */ - uiSchema?: UiSchema; /** Flag indicating whether there are errors associated with this field */ hasErrors?: boolean; - /** The `registry` object */ - registry: Registry; }; /** The set of `Fields` stored in the `Registry` */ @@ -282,8 +276,17 @@ export type RegistryWidgetsType; }; +export type RJSFBaseProps = { + /** The schema object for the field being described */ + schema: S; + /** The uiSchema object for this description field */ + uiSchema?: UiSchema; + /** The `registry` object */ + registry: Registry; +}; + /** The set of RJSF templates that can be overridden by themes or users */ -export interface TemplatesType { +export type TemplatesType = { /** The template to use while rendering normal or fixed array fields */ ArrayFieldTemplate: ComponentType>; /** The template to use while rendering the description for an array field */ @@ -327,7 +330,10 @@ export interface TemplatesType>; }; -} +} & { + /** Allow this to support any named `ComponentType` or an object of named `ComponentType`s */ + [key: string]: ComponentType | { [key: string]: ComponentType }; +}; /** The set of UiSchema options that can be set globally and used as fallbacks at an individual template, field or * widget level when no field-level value of the option is provided. @@ -433,7 +439,11 @@ export type Field; /** The properties that are passed to a FieldTemplate implementation */ -export type FieldTemplateProps = { +export type FieldTemplateProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The id of the field in the hierarchy. You can use it to render a label targeting the wrapped widget */ id: string; /** A string containing the base CSS classes, merged with any custom ones defined in your uiSchema */ @@ -474,10 +484,6 @@ export type FieldTemplateProps; /** The `formContext` object that was passed to `Form` */ formContext?: F; /** The formData for this field */ @@ -488,50 +494,44 @@ export type FieldTemplateProps () => void; /** The property drop/removal event handler; Called when a field is removed in an additionalProperty context */ onDropPropertyClick: (value: string) => () => void; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to the `UnsupportedFieldTemplate` implementation */ -export type UnsupportedFieldProps = { - /** The schema object for this field */ - schema: S; +export type UnsupportedFieldProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The tree of unique ids for every child field */ idSchema?: IdSchema; /** The reason why the schema field has an unsupported type */ reason: string; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to a `TitleFieldTemplate` implementation */ -export type TitleFieldProps = { +export type TitleFieldProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The id of the field title in the hierarchy */ id: string; /** The title for the field being rendered */ title: string; - /** The schema object for the field being titled */ - schema: S; - /** The uiSchema object for this title field */ - uiSchema?: UiSchema; /** A boolean value stating if the field is required */ required?: boolean; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to a `DescriptionFieldTemplate` implementation */ -export type DescriptionFieldProps = { +export type DescriptionFieldProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +> = RJSFBaseProps & { /** The id of the field description in the hierarchy */ id: string; - /** The schema object for the field being described */ - schema: S; - /** The uiSchema object for this description field */ - uiSchema?: UiSchema; /** The description of the field being rendered */ description: string | ReactElement; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to a `ArrayFieldTitleTemplate` implementation */ @@ -563,7 +563,7 @@ export type ArrayFieldTemplateItemType< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any -> = { +> = RJSFBaseProps & { /** The html for the item's content */ children: ReactElement; /** The className string */ @@ -598,12 +598,6 @@ export type ArrayFieldTemplateItemType< readonly?: boolean; /** A stable, unique key for the array item */ key: string; - /** The schema object for this array item */ - schema: S; - /** The uiSchema object for this array item */ - uiSchema?: UiSchema; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to an ArrayFieldTemplate implementation */ @@ -611,7 +605,7 @@ export type ArrayFieldTemplateProps< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any -> = { +> = RJSFBaseProps & { /** A boolean value stating whether new elements can be added to the array */ canAdd?: boolean; /** The className string */ @@ -630,10 +624,6 @@ export type ArrayFieldTemplateProps< required?: boolean; /** A boolean value stating if the field is hiding its errors */ hideError?: boolean; - /** The schema object for this array */ - schema: S; - /** The uiSchema object for this array field */ - uiSchema?: UiSchema; /** A string value containing the title for the array */ title: string; /** The `formContext` object that was passed to Form */ @@ -644,8 +634,6 @@ export type ArrayFieldTemplateProps< errorSchema?: ErrorSchema; /** An array of strings listing all generated error messages from encountered errors for this widget */ rawErrors?: string[]; - /** The `registry` object */ - registry: Registry; }; /** The properties of each element in the ObjectFieldTemplateProps.properties array */ @@ -667,7 +655,7 @@ export type ObjectFieldTemplateProps< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any -> = { +> = RJSFBaseProps & { /** A string value containing the title for the object */ title: string; /** A string value containing the description for the object */ @@ -684,10 +672,6 @@ export type ObjectFieldTemplateProps< required?: boolean; /** A boolean value stating if the field is hiding its errors */ hideError?: boolean; - /** The schema object for this object */ - schema: S; - /** The uiSchema object for this object field */ - uiSchema?: UiSchema; /** An object containing the id for this object & ids for its properties */ idSchema: IdSchema; /** The optional validation errors in the form of an `ErrorSchema` */ @@ -696,8 +680,6 @@ export type ObjectFieldTemplateProps< formData?: T; /** The `formContext` object that was passed to Form */ formContext?: F; - /** The `registry` object */ - registry: Registry; }; /** The properties that are passed to a WrapIfAdditionalTemplate implementation */ @@ -705,24 +687,24 @@ export type WrapIfAdditionalTemplateProps< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any -> = { +> = RJSFBaseProps & { /** The field or widget component instance for this field row */ children: ReactNode; } & Pick< - FieldTemplateProps, - | 'id' - | 'classNames' - | 'style' - | 'label' - | 'required' - | 'readonly' - | 'disabled' - | 'schema' - | 'uiSchema' - | 'onKeyChange' - | 'onDropPropertyClick' - | 'registry' ->; + FieldTemplateProps, + | 'id' + | 'classNames' + | 'style' + | 'label' + | 'required' + | 'readonly' + | 'disabled' + | 'schema' + | 'uiSchema' + | 'onKeyChange' + | 'onDropPropertyClick' + | 'registry' + >; /** The properties that are passed to a Widget implementation */ export interface WidgetProps @@ -793,7 +775,8 @@ export interface BaseInputTemplateProps< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any -> extends WidgetProps { +> extends WidgetProps, + RJSFBaseProps { /** A `BaseInputTemplate` implements a default `onChange` handler that it passes to the HTML input component to handle * the `ChangeEvent`. Sometimes a widget may need to handle the `ChangeEvent` using custom logic. If that is the case, * that widget should provide its own handler via this prop. @@ -814,16 +797,13 @@ export type IconButtonProps< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any -> = ButtonHTMLAttributes & { - /** An alternative specification for the type of the icon button */ - iconType?: string; - /** The name representation or actual react element implementation for the icon */ - icon?: string | ReactElement; - /** The uiSchema for this widget */ - uiSchema?: UiSchema; - /** The `registry` object */ - registry: Registry; -}; +> = ButtonHTMLAttributes & + Omit, 'schema'> & { + /** An alternative specification for the type of the icon button */ + iconType?: string; + /** The name representation or actual react element implementation for the icon */ + icon?: string | ReactElement; + }; /** The type that defines how to change the behavior of the submit button for the form */ export type UISchemaSubmitButtonOptions = { @@ -859,7 +839,23 @@ type MakeUIType = { * remap the keys. It also contains all the properties, optionally, of `TemplatesType` except "ButtonTemplates" */ type UIOptionsBaseType = Partial< - Omit, 'ButtonTemplates'> + Pick< + TemplatesType, + | 'ArrayFieldDescriptionTemplate' + | 'ArrayFieldItemTemplate' + | 'ArrayFieldTemplate' + | 'ArrayFieldTitleTemplate' + | 'BaseInputTemplate' + | 'DescriptionFieldTemplate' + | 'ErrorListTemplate' + | 'FieldErrorTemplate' + | 'FieldHelpTemplate' + | 'FieldTemplate' + | 'ObjectFieldTemplate' + | 'TitleFieldTemplate' + | 'UnsupportedFieldTemplate' + | 'WrapIfAdditionalTemplate' + > > & GlobalUISchemaOptions & { /** Any classnames that the user wants to be applied to a field in the ui */ @@ -998,6 +994,7 @@ export interface ValidatorType, uiSchema?: UiSchema ): ValidationData; + /** Converts an `errorSchema` into a list of `RJSFValidationErrors` * * @param errorSchema - The `ErrorSchema` instance to convert @@ -1006,6 +1003,7 @@ export interface ValidatorType, fieldPath?: string[]): RJSFValidationError[]; + /** Validates data against a schema, returning true if the data is valid, or * false otherwise. If the schema is invalid, then this function will return * false. @@ -1015,6 +1013,7 @@ export interface ValidatorType(schema: S, formData?: T): { errors?: Result[]; validationError?: Error }; + /** An optional function that can be used to reset validator implementation. Useful for clear schemas in the AJV * instance for tests. */ @@ -1039,6 +1039,7 @@ export interface SchemaUtilsType; + /** Determines whether either the `validator` and `rootSchema` differ from the ones associated with this instance of * the `SchemaUtilsType`. If either `validator` or `rootSchema` are falsy, then return false to prevent the creation * of a new `SchemaUtilsType` with incomplete properties. @@ -1055,6 +1056,7 @@ export interface SchemaUtilsType ): boolean; + /** Returns the superset of `formData` that includes the given set updated to include any missing fields that have * computed to have defaults provided in the `schema`. * @@ -1070,6 +1072,7 @@ export interface SchemaUtilsType, globalOptions?: GlobalUISchemaOptions): boolean; + /** Determines which of the given `options` provided most closely matches the `formData`. * Returns the index of the option that is valid and is the closest match, or 0 if there is no match. * @@ -1098,6 +1102,7 @@ export interface SchemaUtilsType): boolean; + /** Checks to see if the `schema` combination represents a multi-select * * @param schema - The schema for which check for a multi-select flag is desired * @returns - True if schema contains a multi-select, otherwise false */ isMultiSelect(schema: S): boolean; + /** Checks to see if the `schema` combination represents a select * * @param schema - The schema for which check for a select flag is desired * @returns - True if schema contains a select, otherwise false */ isSelect(schema: S): boolean; + /** Merges the errors in `additionalErrorSchema` into the existing `validationData` by combining the hierarchies in * the two `ErrorSchema`s and then appending the error list from the `additionalErrorSchema` obtained by calling * `validator.toErrorList()` onto the `errors` in the `validationData`. If no `additionalErrorSchema` is passed, then @@ -1150,6 +1160,7 @@ export interface SchemaUtilsType, additionalErrorSchema?: ErrorSchema): ValidationData; + /** Retrieves an expanded schema that has had all of its conditions, additional properties, references and * dependencies resolved and merged into the `schema` given a `rawFormData` that is used to do the potentially * recursive resolution. @@ -1159,6 +1170,7 @@ export interface SchemaUtilsType; + /** Generates an `PathSchema` object for the `schema`, recursively * * @param schema - The schema for which the display label flag is desired diff --git a/packages/utils/test/getTemplate.test.ts b/packages/utils/test/getTemplate.test.ts index 7b9300475f..231aa3bc0e 100644 --- a/packages/utils/test/getTemplate.test.ts +++ b/packages/utils/test/getTemplate.test.ts @@ -86,4 +86,31 @@ describe('getTemplate', () => { expect(getTemplate(name, registry, uiOptions)).toBe(CustomTemplate); }); }); + it('returns the template from registry using uiOptions key when available', () => { + KEYS.forEach((key) => { + const name = key as keyof TemplatesType; + expect( + getTemplate( + name, + registry, + Object.keys(uiOptions).reduce((uiOptions, key) => { + (uiOptions as Record)[key] = key; + return uiOptions; + }, {}) + ) + ).toBe(FakeTemplate); + }); + }); + it('returns the custom template name from the registry', () => { + const customTemplateKey = 'CustomTemplate'; + registry.templates[customTemplateKey] = FakeTemplate; + + expect(getTemplate(customTemplateKey, registry)).toBe(FakeTemplate); + }); + + it('returns undefined when the custom template is not in the registry', () => { + const customTemplateKey = 'CustomTemplate'; + + expect(getTemplate(customTemplateKey, registry)).toBeUndefined(); + }); });