From e0897434dfac3ddbb4f9bf7d113b79651d058b6f Mon Sep 17 00:00:00 2001 From: Heath C <51679588+heath-freenome@users.noreply.github.com> Date: Fri, 19 Aug 2022 09:48:31 -0700 Subject: [PATCH] Use ui:emptyValue for SelectWidget (#3026) * Use ui:emptyValue for SelectWidget - This is a reimplementation of https://github.com/rjsf-team/react-jsonschema-form/pull/2251 - Fixes #1041 * - Added `processSelectValue()` to `fluent-ui` for single select * - Fixed documentation for `emptyValue` * - Responded to self-feedback --- .../antd/src/widgets/SelectWidget/index.js | 8 ++++--- .../src/SelectWidget/SelectWidget.tsx | 6 +++--- .../src/SelectWidget/SelectWidget.tsx | 9 ++++---- .../src/components/widgets/SelectWidget.tsx | 6 +++--- .../src/SelectWidget/SelectWidget.tsx | 10 +++++---- .../src/SelectWidget/SelectWidget.tsx | 6 +++--- .../mui/src/SelectWidget/SelectWidget.tsx | 6 +++--- .../src/SelectWidget/SelectWidget.js | 6 +++--- packages/utils/src/processSelectValue.ts | 16 ++++++++++---- packages/utils/src/types.ts | 2 ++ .../utils/test/processSelectValue.test.ts | 21 ++++++++++++++++++- 11 files changed, 65 insertions(+), 31 deletions(-) diff --git a/packages/antd/src/widgets/SelectWidget/index.js b/packages/antd/src/widgets/SelectWidget/index.js index bcd14d223a..9b37190710 100644 --- a/packages/antd/src/widgets/SelectWidget/index.js +++ b/packages/antd/src/widgets/SelectWidget/index.js @@ -30,11 +30,13 @@ const SelectWidget = ({ const { enumOptions, enumDisabled } = options; const handleChange = (nextValue) => - onChange(processSelectValue(schema, nextValue)); + onChange(processSelectValue(schema, nextValue, options)); - const handleBlur = () => onBlur(id, processSelectValue(schema, value)); + const handleBlur = () => + onBlur(id, processSelectValue(schema, value, options)); - const handleFocus = () => onFocus(id, processSelectValue(schema, value)); + const handleFocus = () => + onFocus(id, processSelectValue(schema, value, options)); const getPopupContainer = (node) => node.parentNode; diff --git a/packages/bootstrap-4/src/SelectWidget/SelectWidget.tsx b/packages/bootstrap-4/src/SelectWidget/SelectWidget.tsx index 195428978c..90b2d892dc 100644 --- a/packages/bootstrap-4/src/SelectWidget/SelectWidget.tsx +++ b/packages/bootstrap-4/src/SelectWidget/SelectWidget.tsx @@ -60,19 +60,19 @@ const SelectWidget = ({ onBlur && ((event: React.FocusEvent) => { const newValue = getValue(event, multiple); - onBlur(id, processSelectValue(schema, newValue)); + onBlur(id, processSelectValue(schema, newValue, options)); }) } onFocus={ onFocus && ((event: React.FocusEvent) => { const newValue = getValue(event, multiple); - onFocus(id, processSelectValue(schema, newValue)); + onFocus(id, processSelectValue(schema, newValue, options)); }) } onChange={(event: React.ChangeEvent) => { const newValue = getValue(event, multiple); - onChange(processSelectValue(schema, newValue)); + onChange(processSelectValue(schema, newValue, options)); }} > {!multiple && schema.default === undefined && ( diff --git a/packages/chakra-ui/src/SelectWidget/SelectWidget.tsx b/packages/chakra-ui/src/SelectWidget/SelectWidget.tsx index 41324779e8..a0dd3fa169 100644 --- a/packages/chakra-ui/src/SelectWidget/SelectWidget.tsx +++ b/packages/chakra-ui/src/SelectWidget/SelectWidget.tsx @@ -40,7 +40,8 @@ const SelectWidget = (props: WidgetProps) => { schema, e.map((v: { label: any; value: any }) => { return v.value; - }) + }), + options ) ); }; @@ -48,15 +49,15 @@ const SelectWidget = (props: WidgetProps) => { const _onChange = ({ target: { value }, }: React.ChangeEvent<{ name?: string; value: unknown }>) => - onChange(processSelectValue(schema, value)); + onChange(processSelectValue(schema, value, options)); const _onBlur = ({ target: { value }, }: React.FocusEvent) => - onBlur(id, processSelectValue(schema, value)); + onBlur(id, processSelectValue(schema, value, options)); const _onFocus = ({ target: { value }, }: React.FocusEvent) => - onFocus(id, processSelectValue(schema, value)); + onFocus(id, processSelectValue(schema, value, options)); return ( ({ const handleFocus = (event: FocusEvent) => { const newValue = getValue(event, multiple); - return onFocus(id, processSelectValue(schema, newValue)); + return onFocus(id, processSelectValue(schema, newValue, options)); }; const handleBlur = (event: FocusEvent) => { const newValue = getValue(event, multiple); - return onBlur(id, processSelectValue(schema, newValue)); + return onBlur(id, processSelectValue(schema, newValue, options)); }; const handleChange = (event: ChangeEvent) => { const newValue = getValue(event, multiple); - return onChange(processSelectValue(schema, newValue)); + return onChange(processSelectValue(schema, newValue, options)); }; return ( diff --git a/packages/fluent-ui/src/SelectWidget/SelectWidget.tsx b/packages/fluent-ui/src/SelectWidget/SelectWidget.tsx index 742dc34b34..abfb89dcb1 100644 --- a/packages/fluent-ui/src/SelectWidget/SelectWidget.tsx +++ b/packages/fluent-ui/src/SelectWidget/SelectWidget.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Label, Dropdown, IDropdownOption } from "@fluentui/react"; -import { WidgetProps } from "@rjsf/utils"; +import { WidgetProps, processSelectValue } from "@rjsf/utils"; import _pick from "lodash/pick"; // Keys of IDropdownProps from @fluentui/react @@ -80,12 +80,14 @@ const SelectWidget = ({ onChange(valueOrDefault.filter((key: any) => key !== item.key)); } } else { - onChange(item.key); + onChange(processSelectValue(schema, item.key, options)); } }; - const _onBlur = (e: any) => onBlur(id, e.target.value); + const _onBlur = (e: any) => + onBlur(id, processSelectValue(schema, e.target.value, options)); - const _onFocus = (e: any) => onFocus(id, e.target.value); + const _onFocus = (e: any) => + onFocus(id, processSelectValue(schema, e.target.value, options)); const newOptions = (enumOptions as { value: any; label: any }[]).map( (option) => ({ diff --git a/packages/material-ui/src/SelectWidget/SelectWidget.tsx b/packages/material-ui/src/SelectWidget/SelectWidget.tsx index 68dc0ec131..575cdc6e59 100644 --- a/packages/material-ui/src/SelectWidget/SelectWidget.tsx +++ b/packages/material-ui/src/SelectWidget/SelectWidget.tsx @@ -26,13 +26,13 @@ const SelectWidget = ({ const _onChange = ({ target: { value }, }: React.ChangeEvent<{ name?: string; value: unknown }>) => - onChange(processSelectValue(schema, value)); + onChange(processSelectValue(schema, value, options)); const _onBlur = ({ target: { value } }: React.FocusEvent) => - onBlur(id, processSelectValue(schema, value)); + onBlur(id, processSelectValue(schema, value, options)); const _onFocus = ({ target: { value }, }: React.FocusEvent) => - onFocus(id, processSelectValue(schema, value)); + onFocus(id, processSelectValue(schema, value, options)); return ( ) => - onChange(processSelectValue(schema, value)); + onChange(processSelectValue(schema, value, options)); const _onBlur = ({ target: { value } }: React.FocusEvent) => - onBlur(id, processSelectValue(schema, value)); + onBlur(id, processSelectValue(schema, value, options)); const _onFocus = ({ target: { value }, }: React.FocusEvent) => - onFocus(id, processSelectValue(schema, value)); + onFocus(id, processSelectValue(schema, value, options)); return ( onChange && onChange(processSelectValue(schema, value)); + ) => onChange && onChange(processSelectValue(schema, value, options)); // eslint-disable-next-line no-shadow const _onBlur = ({ target: { value } }) => - onBlur && onBlur(id, processSelectValue(schema, value)); + onBlur && onBlur(id, processSelectValue(schema, value, options)); const _onFocus = ({ // eslint-disable-next-line no-shadow target: { value }, - }) => onFocus && onFocus(id, processSelectValue(schema, value)); + }) => onFocus && onFocus(id, processSelectValue(schema, value, options)); return ( (["number", "integer"]); /** Returns the real value for a select widget due to a silly limitation in the DOM which causes option change event - * values to always be retrieved as strings. Uses the `schema` to help determine the value's true type. + * values to always be retrieved as strings. Uses the `schema` to help determine the value's true type. If the value is + * an empty string, then the `emptyValue` from the `options` is returned, falling back to undefined. * * @param schema - The schema to used to determine the value's true type * @param [value] - The value to convert + * @param [options] - The UIOptionsType from which to potentially extract the emptyValue * @returns - The `value` converted to the proper type */ -export default function processSelectValue(schema: RJSFSchema, value?: any) { +export default function processSelectValue( + schema: RJSFSchema, + value?: any, + options?: UIOptionsType +) { const { enum: schemaEnum, type, items } = schema; if (value === "") { - return undefined; + return options && options.emptyValue !== undefined + ? options.emptyValue + : undefined; } if (type === "array" && items && nums.has(get(items, "type"))) { return value.map(asNumber); diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index b83acedb37..690e0576af 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -611,6 +611,8 @@ type UIOptionsBaseType = Partial< autocomplete?: HTMLInputElement["autocomplete"]; /** Flag, if set to `true`, will mark all child widgets from a given field as disabled */ disabled?: boolean; + /** The default value to use when an input for a field is empty */ + emptyValue?: any; /** Will disable any of the enum options specified in the array (by value) */ enumDisabled?: Array; /** Flag, if set to `true`, will hide the default error display for the given field AND all of its child fields in the diff --git a/packages/utils/test/processSelectValue.test.ts b/packages/utils/test/processSelectValue.test.ts index e43b7d38d1..059485ebe3 100644 --- a/packages/utils/test/processSelectValue.test.ts +++ b/packages/utils/test/processSelectValue.test.ts @@ -1,9 +1,28 @@ import { processSelectValue, RJSFSchema } from "../src"; describe("processSelectValue", () => { - it("always returns undefined for an empty string value", () => { + it("always returns undefined for an empty string value with no options", () => { expect(processSelectValue({}, "")).toBeUndefined(); }); + it("always returns undefined for an empty string value with options.emptyValue", () => { + expect(processSelectValue({}, "", {})).toBeUndefined(); + }); + it("always returns options.emptyValue for an empty string value when present and empty string", () => { + const options = { emptyValue: "" }; + expect(processSelectValue({}, "", options)).toBe(options.emptyValue); + }); + it("always returns options.emptyValue for an empty string value when present and null", () => { + const options = { emptyValue: null }; + expect(processSelectValue({}, "", options)).toBe(options.emptyValue); + }); + it("always returns options.emptyValue for an empty string value when present and zero", () => { + const options = { emptyValue: 0 }; + expect(processSelectValue({}, "", options)).toBe(options.emptyValue); + }); + it("always returns options.emptyValue for an empty string value when present and truthy", () => { + const options = { emptyValue: "default value" }; + expect(processSelectValue({}, "", options)).toBe(options.emptyValue); + }); it("returns an array of numbers when the type is array and items represents a number", () => { const schema: RJSFSchema = { type: "array",