diff --git a/CHANGELOG.md b/CHANGELOG.md index 1964ab617c..0ecc785764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,18 @@ 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.25.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.24.9 ## Dev / docs / playground diff --git a/packages/docs/docs/advanced-customization/custom-templates.md b/packages/docs/docs/advanced-customization/custom-templates.md index c2ecb0d11e..a7854ac9fa 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. @@ -262,13 +284,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:ArrayFieldDescriptionTemplate': ArrayFieldDescriptionTemplate, +}; +``` + +or a string value from the `Registry` : ```tsx import { UiSchema } from '@rjsf/utils'; const uiSchema: UiSchema = { - 'ui:ArrayFieldTitleTemplate': ArrayFieldTitleTemplate, + 'ui:ArrayFieldDescriptionTemplate': 'CustomArrayFieldDescriptionTemplate', }; ``` @@ -616,16 +649,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: @@ -694,16 +738,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`: @@ -1050,3 +1105,20 @@ The following prop is passed to a `SubmitButton`: - `uiSchema`: The uiSchema object for this field, used to extract the `UISchemaSubmitButtonOptions`. - `registry`: The `registry` object. + +## Custom Templates + +You can now add custom components to the registry and reference them in your `uiSchema` using string keys. + +### Adding Custom Templates to the Registry + +```tsx +import CustomArrayFieldTemplate from './CustomArrayFieldTemplate'; +import { UiSchema } from '@rjsf/utils'; + +// Add the custom template to the registry +const registry = { templates: { CustomArrayFieldTemplate } }; + +// Use the custom template in the uiSchema +const uiSchema: UiSchema = { 'ui:ArrayFieldTemplate': 'CustomArrayFieldTemplate' }; +``` diff --git a/packages/utils/src/getTemplate.ts b/packages/utils/src/getTemplate.ts index 8863df65d5..ab57d45f65 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.prototype.hasOwnProperty.call(uiOptions, name) && + typeof uiOptions[name] === 'string' && + Object.prototype.hasOwnProperty.call(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 2d6c8335c3..76d8961993 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -236,52 +236,56 @@ export type FormValidation = FieldValidation & { [key in keyof T]?: FormValidation; }; +/** The base properties passed to various RJSF components. */ +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 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` */ @@ -297,7 +301,7 @@ export type RegistryWidgetsType { +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 */ @@ -341,7 +345,10 @@ export interface TemplatesType>; }; -} +} & { + /** Allow this to support any named `ComponentType` or an object of named `ComponentType`s */ + [key: string]: ComponentType | { [key: string]: ComponentType } | undefined; +}; /** 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. @@ -447,7 +454,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 */ @@ -488,10 +499,6 @@ export type FieldTemplateProps; /** The `formContext` object that was passed to `Form` */ formContext?: F; /** The formData for this field */ @@ -502,50 +509,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 */ @@ -577,7 +578,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 */ @@ -612,12 +613,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 */ @@ -625,7 +620,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 */ @@ -644,10 +639,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 */ @@ -658,8 +649,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 */ @@ -681,7 +670,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 */ @@ -698,10 +687,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` */ @@ -710,8 +695,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 */ @@ -719,24 +702,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 @@ -807,7 +790,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. @@ -828,16 +812,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 = { @@ -873,7 +854,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 */ diff --git a/packages/utils/test/getTemplate.test.ts b/packages/utils/test/getTemplate.test.ts index 7b9300475f..8a3798d825 100644 --- a/packages/utils/test/getTemplate.test.ts +++ b/packages/utils/test/getTemplate.test.ts @@ -8,6 +8,7 @@ import { UIOptionsType, } from '../src'; import getTestValidator from './testUtils/getTestValidator'; +import cloneDeep from 'lodash/cloneDeep'; const FakeTemplate = () => null; @@ -86,4 +87,33 @@ 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'; + const newRegistry = cloneDeep(registry); + + newRegistry.templates[customTemplateKey] = FakeTemplate; + + expect(getTemplate(customTemplateKey, newRegistry)).toBe(FakeTemplate); + }); + + it('returns undefined when the custom template is not in the registry', () => { + const customTemplateKey = 'CustomTemplate'; + + expect(getTemplate(customTemplateKey, registry)).toBeUndefined(); + }); });