diff --git a/CHANGELOG.md b/CHANGELOG.md index 447b20576a..c7c4bb32f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,9 @@ should change the heading of the (upcoming) version to include a major version b - BREAKING CHANGE: Removed the deprecated `RJSF_ADDITONAL_PROPERTIES_FLAG` constant - Updated the `WrapIfAdditionalTemplateProps` to include `hideError` and `rawErrors` in support of moving `Bootstrap 3` marker classes out of `SchemaField` - Added support for `patternProperties` [#1944](https://github.com/rjsf-team/react-jsonschema-form/issues/1944) +- 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 ## @rjsf/validator-ajv6 @@ -134,6 +137,7 @@ should change the heading of the (upcoming) version to include a major version b - Replaced Lerna with Nx, updated all lerna commands to use the Nx CLI - BREAKING CHANGE: Updated all `peerDependencies` to change minimal `React` support to `>=18` - Added documentation and playground example for `patternProperties` +- Updated `advanced-customization/custom-templates` with the new feature. # 6.0.0-alpha.0 diff --git a/packages/docs/docs/advanced-customization/custom-templates.md b/packages/docs/docs/advanced-customization/custom-templates.md index b5dea68552..701b064f91 100644 --- a/packages/docs/docs/advanced-customization/custom-templates.md +++ b/packages/docs/docs/advanced-customization/custom-templates.md @@ -78,16 +78,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`: @@ -165,16 +176,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. @@ -322,13 +344,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', }; ``` @@ -676,16 +709,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: @@ -789,16 +833,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`: @@ -1145,3 +1200,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 0df17037b6..0ec112e6bb 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 8fad986281..7b5b38588b 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -241,52 +241,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 properties that are passed to a `GridTemplate` */ @@ -312,7 +316,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 */ @@ -360,7 +364,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. @@ -466,7 +473,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 */ @@ -507,10 +518,6 @@ export type FieldTemplateProps; /** The `formContext` object that was passed to `Form` */ formContext?: F; /** The formData for this field */ @@ -521,50 +528,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 */ @@ -596,7 +597,7 @@ export type ArrayFieldItemButtonsTemplateType< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any, -> = { +> = RJSFBaseProps & { /** The idSchema of the item for which buttons are being rendered */ idSchema: IdSchema; /** The className string */ @@ -629,12 +630,6 @@ export type ArrayFieldItemButtonsTemplateType< onReorderClick: (index: number, newIndex: number) => (event?: any) => void; /** A boolean value stating if the array item is read-only */ readonly?: boolean; - /** 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 of each element in the ArrayFieldTemplateProps.items array */ @@ -642,7 +637,7 @@ export type ArrayFieldItemTemplateType< T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any, -> = { +> = RJSFBaseProps & { /** The html for the item's content */ children: ReactNode; /** The props to pass to the `ArrayFieldItemButtonTemplate` */ @@ -661,12 +656,6 @@ export type ArrayFieldItemTemplateType< 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; }; /** @@ -683,7 +672,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 */ @@ -702,10 +691,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 */ @@ -716,8 +701,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 */ @@ -739,7 +722,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 */ @@ -756,10 +739,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` */ @@ -768,8 +747,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 */ @@ -777,30 +754,31 @@ 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' - | 'hideError' - | 'rawErrors' - | 'style' - | 'label' - | 'required' - | 'readonly' - | 'disabled' - | 'schema' - | 'uiSchema' - | 'onKeyChange' - | 'onDropPropertyClick' - | 'registry' ->; + FieldTemplateProps, + | 'id' + | 'classNames' + | 'hideError' + | 'rawErrors' + | 'style' + | 'label' + | 'required' + | 'readonly' + | 'disabled' + | 'schema' + | 'uiSchema' + | 'onKeyChange' + | 'onDropPropertyClick' + | 'registry' + >; /** The properties that are passed to a Widget implementation */ export interface WidgetProps extends GenericObjectType, + RJSFBaseProps, Pick, Exclude, 'onBlur' | 'onFocus'>> { /** The generated id for this widget, used to provide unique `name`s and `id`s for the HTML field elements rendered by * widgets @@ -810,10 +788,6 @@ export interface WidgetProps; /** The current value for this widget */ value: any; /** The required status of this widget */ @@ -853,8 +827,6 @@ export interface WidgetProps; } /** The definition of a React-based Widget component */ @@ -888,16 +860,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 = { @@ -933,7 +902,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 & { /** Allows RJSF to override the default field implementation by specifying either the name of a field that is used diff --git a/packages/utils/test/getTemplate.test.ts b/packages/utils/test/getTemplate.test.ts index bf19911530..550297d514 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; @@ -90,4 +91,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(); + }); });