Skip to content

Commit

Permalink
Use ui:emptyValue for SelectWidget (rjsf-team#3026)
Browse files Browse the repository at this point in the history
* Use ui:emptyValue for SelectWidget
- This is a reimplementation of rjsf-team#2251
- Fixes rjsf-team#1041

* - Added `processSelectValue()` to `fluent-ui` for single select

* - Fixed documentation for `emptyValue`

* - Responded to self-feedback
  • Loading branch information
heath-freenome committed Aug 27, 2022
1 parent bc5d036 commit e089743
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 31 deletions.
8 changes: 5 additions & 3 deletions packages/antd/src/widgets/SelectWidget/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions packages/bootstrap-4/src/SelectWidget/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand Down
9 changes: 5 additions & 4 deletions packages/chakra-ui/src/SelectWidget/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,24 @@ const SelectWidget = (props: WidgetProps) => {
schema,
e.map((v: { label: any; value: any }) => {
return v.value;
})
}),
options
)
);
};

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<HTMLSelectElement>) =>
onBlur(id, processSelectValue(schema, value));
onBlur(id, processSelectValue(schema, value, options));
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLSelectElement>) =>
onFocus(id, processSelectValue(schema, value));
onFocus(id, processSelectValue(schema, value, options));

return (
<FormControl
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/widgets/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ function SelectWidget<T = any, F = any>({

const handleFocus = (event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onFocus(id, processSelectValue(schema, newValue));
return onFocus(id, processSelectValue(schema, newValue, options));
};

const handleBlur = (event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onBlur(id, processSelectValue(schema, newValue));
return onBlur(id, processSelectValue(schema, newValue, options));
};

const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onChange(processSelectValue(schema, newValue));
return onChange(processSelectValue(schema, newValue, options));
};

return (
Expand Down
10 changes: 6 additions & 4 deletions packages/fluent-ui/src/SelectWidget/SelectWidget.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) => ({
Expand Down
6 changes: 3 additions & 3 deletions packages/material-ui/src/SelectWidget/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>) =>
onBlur(id, processSelectValue(schema, value));
onBlur(id, processSelectValue(schema, value, options));
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, processSelectValue(schema, value));
onFocus(id, processSelectValue(schema, value, options));

return (
<TextField
Expand Down
6 changes: 3 additions & 3 deletions packages/mui/src/SelectWidget/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>) =>
onBlur(id, processSelectValue(schema, value));
onBlur(id, processSelectValue(schema, value, options));
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, processSelectValue(schema, value));
onFocus(id, processSelectValue(schema, value, options));

return (
<TextField
Expand Down
6 changes: 3 additions & 3 deletions packages/semantic-ui/src/SelectWidget/SelectWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ function SelectWidget(props) {
event,
// eslint-disable-next-line no-shadow
{ value }
) => 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 (
<Form.Dropdown
Expand Down
16 changes: 12 additions & 4 deletions packages/utils/src/processSelectValue.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import get from "lodash/get";

import { RJSFSchema } from "./types";
import { RJSFSchema, UIOptionsType } from "./types";
import asNumber from "./asNumber";
import guessType from "./guessType";

const nums = new Set<any>(["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<T = any, F = any>(
schema: RJSFSchema,
value?: any,
options?: UIOptionsType<T, F>
) {
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);
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,8 @@ type UIOptionsBaseType<T = any, F = any> = 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<string | number | boolean>;
/** Flag, if set to `true`, will hide the default error display for the given field AND all of its child fields in the
Expand Down
21 changes: 20 additions & 1 deletion packages/utils/test/processSelectValue.test.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down

0 comments on commit e089743

Please sign in to comment.