From 5db71e39d1a68f76393690b27d63c7c8b28d7312 Mon Sep 17 00:00:00 2001 From: Heath C <51679588+heath-freenome@users.noreply.github.com> Date: Sun, 15 Jan 2023 10:53:47 -0800 Subject: [PATCH] fix: Add support for custom styles in a manner similar to classNames (#3378) * fix: Add support for custom styles in a manner similar to classNames Fixes #1200 by reimplementing #1256 - In `@rjsf/utils`, added the new `style` prop onto `FieldTemplateProps`, `WrapIfAdditionalTemplateProps` and `UIOptionsBaseType` - In `@rjsf/core`, added support for the new `style` prop in `uiSchema` as follows: - Updated `SchemaField` to handle the new `style` prop in the `uiSchema` similarly to `classNames`, passing it to the `FieldTemplate` and removing it from being passed down to children. - Also, added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper - Added or updated tests to verify the `style` prop functionality - In all the themes, added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper - Fluent-ui is a special case since it doesn't currently implement `WrapIfAdditionalTemplate` fully - Updated the documentation to describe this new `style` prop - Also updated the `validation` documentation to describe the `uiSchema` prop that can be passed to the `customValidate()` and `transformError()` functions - Updated the `CHANGELOG.md` accordingly * - Renamed `styles` to `style` * Apply suggestions from code review Co-authored-by: Nick Grosenbacher Co-authored-by: Nick Grosenbacher --- CHANGELOG.md | 29 ++++++++++++++- .../custom-templates.md | 10 +++-- docs/api-reference/uiSchema.md | 24 ++++++++++++ docs/usage/validation.md | 6 ++- .../src/templates/FieldTemplate/index.tsx | 2 + .../WrapIfAdditionalTemplate/index.tsx | 9 ++++- .../src/FieldTemplate/FieldTemplate.tsx | 2 + .../WrapIfAdditionalTemplate.tsx | 9 ++++- .../src/FieldTemplate/FieldTemplate.tsx | 2 + .../WrapIfAdditionalTemplate.tsx | 15 +++++++- .../src/components/fields/SchemaField.tsx | 15 ++++++-- .../templates/WrapIfAdditionalTemplate.tsx | 9 ++++- packages/core/test/SchemaField_test.js | 8 +++- packages/core/test/uiSchema_test.js | 37 +++++++++++++++++++ .../src/FieldTemplate/FieldTemplate.tsx | 2 + .../WrapIfAdditionalTemplate.tsx | 8 +++- .../mui/src/FieldTemplate/FieldTemplate.tsx | 2 + .../WrapIfAdditionalTemplate.tsx | 8 +++- .../src/FieldTemplate/FieldTemplate.tsx | 2 + .../WrapIfAdditionalTemplate.tsx | 9 ++++- packages/utils/src/types.ts | 7 +++- 21 files changed, 189 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3b79121e..0910098483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,20 +19,45 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/antd - Enable searching in the `SelectWidget` by the label that the user sees rather than by the value +- Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) + +## @rjsf/bootstrap-4 +- Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) + +## @rjsf/chakra-ui +- Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) + +## @rjsf/core +- Updated `SchemaField` to handle the new `style` prop in the `uiSchema` similarly to `classNames`, passing it to the `FieldTemplate` and removing it from being passed down to children. + - Also, added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper + - This partially fixes [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) + +## @rjsf/fluent-ui +- Added support for new `style` prop on `FieldTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) ## @rjsf/material-ui - Updated `SelectWidget` to support additional `TextFieldProps` in a manner similar to how `BaseInputTemplate` does +- Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) ## @rjsf/mui - Updated `SelectWidget` to support additional `TextFieldProps` in a manner similar to how `BaseInputTemplate` does +- Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) -## @rjsf/playground -- Change Vite `preserveSymlinks` to `true`, which provides an alternative fix for [#3228](https://github.com/rjsf-team/react-jsonschema-form/issues/3228) since the prior fix caused [#3215](https://github.com/rjsf-team/react-jsonschema-form/issues/3215). +## @rjsf/semantic-ui +- Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) + +## @rjsf/utils +- Updated the `FieldTemplateProps`, `WrapIfAdditionalTemplateProps` and `UIOptionsBaseType` types to add `style?: StyleHTMLAttributes`, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) ## @rjsf/validator-ajv8 - Remove alias for ajv -> ajv8 in package.json. This fixes [#3215](https://github.com/rjsf-team/react-jsonschema-form/issues/3215). - Updated `AJV8Validator#transformRJSFValidationErrors` to return more human readable error messages. The ajv8 `ErrorObject` message is enhanced by replacing the error message field with either the `uiSchema`'s `ui:title` field if one exists or the `parentSchema` title if one exists. Fixes [#3246](https://github.com/rjsf-team/react-jsonschema-form/issues/3246) +## Dev / docs / playground +- In the playground, change Vite `preserveSymlinks` to `true`, which provides an alternative fix for [#3228](https://github.com/rjsf-team/react-jsonschema-form/issues/3228) since the prior fix caused [#3215](https://github.com/rjsf-team/react-jsonschema-form/issues/3215). +- Updated the `custom-templates.md` and `uiSchema.md` to document the new `style` prop +- Updated the `validation.md` documentation to describe the new `uiSchema` parameter passed to the `customValidate()` and `transformError()` functions + # 5.0.0-beta-16 ## @rjsf/antd diff --git a/docs/advanced-customization/custom-templates.md b/docs/advanced-customization/custom-templates.md index 5ee3819a17..661c67cbab 100644 --- a/docs/advanced-customization/custom-templates.md +++ b/docs/advanced-customization/custom-templates.md @@ -561,9 +561,9 @@ const schema: RJSFSchema = { }; function CustomFieldTemplate(props: FieldTemplateProps) { - const {id, classNames, label, help, required, description, errors, children} = props; + const {id, classNames, style, label, help, required, description, errors, children} = props; return ( -
+
{description} {children} @@ -594,6 +594,7 @@ The following props are passed to a custom field template component: - `id`: The id of the field in the hierarchy. You can use it to render a label targeting the wrapped widget. - `classNames`: A string containing the base Bootstrap CSS classes, merged with any [custom ones](#custom-css-class-names) defined in your uiSchema. +- `style`: An object containing the `StyleHTMLAttributes` defined in the `uiSchema`. - `label`: The computed label for this field, as a string. - `description`: A component instance rendering the field description, if one is defined (this will use any [custom `DescriptionField`](#custom-descriptions) defined). - `rawDescription`: A string containing any `ui:description` uiSchema directive defined. @@ -793,6 +794,8 @@ function WrapIfAdditionalTemplate( children, uiSchema, registry, + classNames, + style, } = props; const { RemoveButton } = registry.templates.ButtonTemplates; const additional = ADDITIONAL_PROPERTY_FLAG in schema; @@ -802,7 +805,7 @@ function WrapIfAdditionalTemplate( } return ( -
+
``` +### style + +The uiSchema object accepts a `ui:style` property for each field of the schema: + +```tsx +import { UiSchema } from "@rjsf/utils"; + +const uiSchema = { + title: { + "ui:style": { color: "red" } + } +}; +``` + +Will result in: + +```html +
+ +
+``` ### autocomplete diff --git a/docs/usage/validation.md b/docs/usage/validation.md index 6323c99acc..7ffa693a70 100644 --- a/docs/usage/validation.md +++ b/docs/usage/validation.md @@ -99,7 +99,7 @@ This is especially useful when the validation depends on several interdependent import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv8"; -function customValidate(formData, errors) { +function customValidate(formData, errors, uiSchema) { if (formData.pass1 !== formData.pass2) { errors.pass2.addError("Passwords don't match"); } @@ -123,6 +123,7 @@ render(( > - The `customValidate()` function must implement the `CustomValidator` interface found in `@rjsf/utils`. > - The `customValidate()` function must **always** return the `errors` object received as second argument. > - The `customValidate()` function is called **after** the JSON schema validation. +> - The `customValidate()` function is passed the `uiSchema` as the third argument. This allows the `customValidate()` function to be able to derive additional information from it for generating errors. ## Custom error messages @@ -133,7 +134,7 @@ If you need to change these messages or make any other modifications to the erro import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv8"; -function transformErrors(errors) { +function transformErrors(errors, uiSchema) { return errors.map(error => { if (error.name === "pattern") { error.message = "Only digits are allowed" @@ -157,6 +158,7 @@ render(( > Notes: > - The `transformErrors()` function must implement the `ErrorTransformer` interface found in `@rjsf/utils`. > - The `transformErrors()` function must return the list of errors. Modifying the list in place without returning it will result in an error. +> - The `transformErrors()` function is passed the `uiSchema` as the second argument. This allows the `transformErrors()` function to be able to derive additional information from it for transforming errors. Each element in the `errors` list passed to `transformErrors` is a `RJSFValidationError` interface (in `@rjsf/utils`) and has the following properties: diff --git a/packages/antd/src/templates/FieldTemplate/index.tsx b/packages/antd/src/templates/FieldTemplate/index.tsx index b672734784..48019152e2 100644 --- a/packages/antd/src/templates/FieldTemplate/index.tsx +++ b/packages/antd/src/templates/FieldTemplate/index.tsx @@ -26,6 +26,7 @@ export default function FieldTemplate< const { children, classNames, + style, description, disabled, displayLabel, @@ -68,6 +69,7 @@ export default function FieldTemplate< return ( {children}
; + return ( +
+ {children} +
+ ); } const handleBlur = ({ target }: React.FocusEvent) => @@ -73,7 +78,7 @@ export default function WrapIfAdditionalTemplate< }; return ( -
+
diff --git a/packages/bootstrap-4/src/FieldTemplate/FieldTemplate.tsx b/packages/bootstrap-4/src/FieldTemplate/FieldTemplate.tsx index 8af007dfbf..70279e5f94 100644 --- a/packages/bootstrap-4/src/FieldTemplate/FieldTemplate.tsx +++ b/packages/bootstrap-4/src/FieldTemplate/FieldTemplate.tsx @@ -22,6 +22,7 @@ export default function FieldTemplate< help, rawDescription, classNames, + style, disabled, label, hidden, @@ -46,6 +47,7 @@ export default function FieldTemplate< return ( ({ classNames, + style, children, disabled, id, @@ -36,7 +37,11 @@ export default function WrapIfAdditionalTemplate< const additional = ADDITIONAL_PROPERTY_FLAG in schema; if (!additional) { - return
{children}
; + return ( +
+ {children} +
+ ); } const handleBlur = ({ target }: React.FocusEvent) => @@ -44,7 +49,7 @@ export default function WrapIfAdditionalTemplate< const keyId = `${id}-key`; return ( - + {keyLabel} diff --git a/packages/chakra-ui/src/FieldTemplate/FieldTemplate.tsx b/packages/chakra-ui/src/FieldTemplate/FieldTemplate.tsx index 8cc0243f68..a769b62d1c 100644 --- a/packages/chakra-ui/src/FieldTemplate/FieldTemplate.tsx +++ b/packages/chakra-ui/src/FieldTemplate/FieldTemplate.tsx @@ -18,6 +18,7 @@ export default function FieldTemplate< id, children, classNames, + style, disabled, displayLabel, hidden, @@ -49,6 +50,7 @@ export default function FieldTemplate< return ( {children}
; + return ( +
+ {children} +
+ ); } const keyLabel = `${label} Key`; @@ -45,7 +50,13 @@ export default function WrapIfAdditionalTemplate< onKeyChange(target.value); return ( - + diff --git a/packages/core/src/components/fields/SchemaField.tsx b/packages/core/src/components/fields/SchemaField.tsx index efbb7bf823..99f1481a78 100644 --- a/packages/core/src/components/fields/SchemaField.tsx +++ b/packages/core/src/components/fields/SchemaField.tsx @@ -16,6 +16,7 @@ import { UIOptionsType, ID_KEY, ADDITIONAL_PROPERTY_FLAG, + UI_OPTIONS_KEY, } from "@rjsf/utils"; import isObject from "lodash/isObject"; import omit from "lodash/omit"; @@ -188,11 +189,16 @@ function SchemaFieldRender< const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema); const { __errors, ...fieldErrorSchema } = errorSchema || {}; - // See #439: uiSchema: Don't pass consumed class names to child components - const fieldUiSchema = omit(uiSchema, ["ui:classNames", "classNames"]); - if ("ui:options" in fieldUiSchema) { - fieldUiSchema["ui:options"] = omit(fieldUiSchema["ui:options"], [ + // See #439: uiSchema: Don't pass consumed class names or style to child components + const fieldUiSchema = omit(uiSchema, [ + "ui:classNames", + "classNames", + "ui:style", + ]); + if (UI_OPTIONS_KEY in fieldUiSchema) { + fieldUiSchema[UI_OPTIONS_KEY] = omit(fieldUiSchema[UI_OPTIONS_KEY], [ "classNames", + "style", ]); } @@ -297,6 +303,7 @@ function SchemaFieldRender< hideError, displayLabel, classNames: classNames.join(" ").trim(), + style: uiOptions.style, formContext, formData, schema, diff --git a/packages/core/src/components/templates/WrapIfAdditionalTemplate.tsx b/packages/core/src/components/templates/WrapIfAdditionalTemplate.tsx index bda1bc6450..ef559c4a2c 100644 --- a/packages/core/src/components/templates/WrapIfAdditionalTemplate.tsx +++ b/packages/core/src/components/templates/WrapIfAdditionalTemplate.tsx @@ -22,6 +22,7 @@ export default function WrapIfAdditionalTemplate< const { id, classNames, + style, disabled, label, onKeyChange, @@ -39,11 +40,15 @@ export default function WrapIfAdditionalTemplate< const additional = ADDITIONAL_PROPERTY_FLAG in schema; if (!additional) { - return
{children}
; + return ( +
+ {children} +
+ ); } return ( -
+
diff --git a/packages/core/test/SchemaField_test.js b/packages/core/test/SchemaField_test.js index 98cdebf765..109597b858 100644 --- a/packages/core/test/SchemaField_test.js +++ b/packages/core/test/SchemaField_test.js @@ -203,7 +203,7 @@ describe("SchemaField", () => { expect(node.querySelectorAll("#custom")).to.have.length.of(1); }); - it("should not pass ui:classNames to child component", () => { + it("should not pass ui:classNames or ui:style to child component", () => { const CustomSchemaField = function (props) { return ( { const uiSchema = { "ui:field": "customSchemaField", "ui:classNames": "foo", + "ui:style": { color: "red" }, }; const fields = { customSchemaField: CustomSchemaField }; const { node } = createFormComponent({ schema, uiSchema, fields }); expect(node.querySelectorAll(".foo")).to.have.length.of(1); + expect(node.querySelectorAll("[style*='red']")).to.have.length.of(1); }); - it("should not pass ui:options.classNames to child component", () => { + it("should not pass ui:options { classNames or style } to child component", () => { const CustomSchemaField = function (props) { return ( { "ui:field": "customSchemaField", "ui:options": { classNames: "foo", + style: { color: "red" }, }, }; const fields = { customSchemaField: CustomSchemaField }; @@ -250,6 +253,7 @@ describe("SchemaField", () => { const { node } = createFormComponent({ schema, uiSchema, fields }); expect(node.querySelectorAll(".foo")).to.have.length.of(1); + expect(node.querySelectorAll("[style*='red']")).to.have.length.of(1); }); }); diff --git a/packages/core/test/uiSchema_test.js b/packages/core/test/uiSchema_test.js index 2d0ca37565..ac856c1139 100644 --- a/packages/core/test/uiSchema_test.js +++ b/packages/core/test/uiSchema_test.js @@ -67,6 +67,43 @@ describe("uiSchema", () => { }); }); + describe("custom style", () => { + const schema = { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "string", + }, + }, + }; + + const uiSchema = { + foo: { + "ui:style": { + paddingRight: "1em", + }, + }, + bar: { + "ui:style": { + paddingLeft: "1.5em", + color: "orange", + }, + }, + }; + + it("should apply custom style to target widgets", () => { + const { node } = createFormComponent({ schema, uiSchema }); + const [foo, bar] = node.querySelectorAll(".field-string"); + + expect(foo.style.paddingRight).eql("1em"); + expect(bar.style.paddingLeft).eql("1.5em"); + expect(bar.style.color).eql("orange"); + }); + }); + describe("custom widget", () => { describe("root widget", () => { const schema = { diff --git a/packages/material-ui/src/FieldTemplate/FieldTemplate.tsx b/packages/material-ui/src/FieldTemplate/FieldTemplate.tsx index da74bf87ca..f3d0f88f7c 100644 --- a/packages/material-ui/src/FieldTemplate/FieldTemplate.tsx +++ b/packages/material-ui/src/FieldTemplate/FieldTemplate.tsx @@ -24,6 +24,7 @@ export default function FieldTemplate< id, children, classNames, + style, disabled, displayLabel, hidden, @@ -54,6 +55,7 @@ export default function FieldTemplate< return ( {children}
; + return ( +
+ {children} +
+ ); } const handleBlur = ({ target }: React.FocusEvent) => @@ -60,6 +65,7 @@ export default function WrapIfAdditionalTemplate< alignItems="center" spacing={2} className={classNames} + style={style} > diff --git a/packages/mui/src/FieldTemplate/FieldTemplate.tsx b/packages/mui/src/FieldTemplate/FieldTemplate.tsx index bcc7d0d7b5..c7ac42498c 100644 --- a/packages/mui/src/FieldTemplate/FieldTemplate.tsx +++ b/packages/mui/src/FieldTemplate/FieldTemplate.tsx @@ -24,6 +24,7 @@ export default function FieldTemplate< id, children, classNames, + style, disabled, displayLabel, hidden, @@ -54,6 +55,7 @@ export default function FieldTemplate< return ( {children}
; + return ( +
+ {children} +
+ ); } const handleBlur = ({ target }: React.FocusEvent) => @@ -60,6 +65,7 @@ export default function WrapIfAdditionalTemplate< alignItems="center" spacing={2} className={classNames} + style={style} > diff --git a/packages/semantic-ui/src/FieldTemplate/FieldTemplate.tsx b/packages/semantic-ui/src/FieldTemplate/FieldTemplate.tsx index 038747dc6b..0d505f1073 100644 --- a/packages/semantic-ui/src/FieldTemplate/FieldTemplate.tsx +++ b/packages/semantic-ui/src/FieldTemplate/FieldTemplate.tsx @@ -24,6 +24,7 @@ export default function FieldTemplate< id, children, classNames, + style, displayLabel, label, errors, @@ -58,6 +59,7 @@ export default function FieldTemplate< return ( {children}
; + return ( +
+ {children} +
+ ); } const handleBlur = ({ target }: React.FocusEvent) => onKeyChange(target.value); return ( -
+
diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index d77a3b7cb3..ad1d7637d2 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1,4 +1,4 @@ -import React from "react"; +import React, { StyleHTMLAttributes } from "react"; import { JSONSchema7 } from "json-schema"; /** The representation of any generic object type, usually used as an intersection on other types to make them more @@ -358,6 +358,8 @@ export type FieldTemplateProps< id: string; /** A string containing the base CSS classes, merged with any custom ones defined in your uiSchema */ classNames?: string; + /** An object containing the style as defined in the `uiSchema` */ + style?: StyleHTMLAttributes; /** The computed label for this field, as a string */ label: string; /** A component instance rendering the field description, if one is defined (this will use any custom @@ -634,6 +636,7 @@ export type WrapIfAdditionalTemplateProps< FieldTemplateProps, | "id" | "classNames" + | "style" | "label" | "required" | "readonly" @@ -775,6 +778,8 @@ type UIOptionsBaseType< > = Partial, "ButtonTemplates">> & { /** Any classnames that the user wants to be applied to a field in the ui */ classNames?: string; + /** Any custom style that the user wants to apply to a field in the ui, applied on the same element as classNames */ + style?: StyleHTMLAttributes; /** We know that for title, it will be a string, if it is provided */ title?: string; /** We know that for description, it will be a string, if it is provided */