Skip to content

Commit

Permalink
Fix: switch enumOptions rendering widgets to use indexes
Browse files Browse the repository at this point in the history
Fixes rjsf-team#1494, reimplementing rjsf-team#1562 to make all widgets that render `enumOptions` switch from option value to option index
- In `@rjsf/utils`, added/updated `enumOptionsXXXX` methods that enable using index instead of values
  - Added 100% unit tests for these new methods
  - Also remove the now unnecessary `processSelectValue()` function and its unit test
- In all themes, updated the `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use the `enumOptions[]` index rather than value for the html elements
  - Utilized the new `enumOptionsXXXX` methods to facilitate this transformation
  - Updated the tests in `core` and snapshots in the other themes to represent this change from value to index
- Updated the documentation for the new methods and the breaking change this causes
- Updated the `CHANGELOG.md` file accordingly
  • Loading branch information
heath-freenome committed Jan 30, 2023
1 parent aa07da3 commit de5f153
Show file tree
Hide file tree
Showing 60 changed files with 1,092 additions and 659 deletions.
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,42 @@ 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.0.0

## @rjsf/antd
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/bootstrap-4
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/chakra-ui
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/core
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/fluent-ui
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/material-ui
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/mui
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/semantic-ui
- Updated `CheckboxesWidget`, `RadioWidget` and `SelectWidget` to use indexes as values to support `enumOptions` with object values, fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)

## @rjsf/utils
- Added `enumOptionsIndexForValue()`, `enumOptionsIsSelected()`, `enumOptionsValueForIndex()` functions to support fixing [#1494](https://github.com/rjsf-team/react-jsonschema-form/issues/1494)
- Updated `enumOptionsDeselectValue()` and `enumOptionsSelectValue()` to use indexes instead of values
- Deleted the `processSelectValue()` that was added in the beta and is no longer needed

## Dev / docs / playground
- Updated the `utility-functions` documentation for the new and updated methods mentioned above, as well as deleting the documentation for `processSelectValue()`
- Updated the playground to add a new `Enum Objects` example to highlight the use of indexes for `enumOptions`
- Updated `5.x migration guide` to document the change from values to indexes for the `enumOptions` based controls.

# 5.0.0-beta.20

## @rjsf/antd
Expand Down
9 changes: 8 additions & 1 deletion docs/5.x upgrade guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Unfortunately, there is required work pending to properly support React 18, so u
There are four new packages added in RJSF version 5:

- `@rjsf/utils`: All of the [utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions) previously imported from `@rjsf/core/utils` as well as the Typescript types for RJSF version 5.
- The following new utility functions were added: `ariaDescribedByIds()`, `createSchemaUtils()`, `descriptionId()`, `enumOptionsDeselectValue()`, `enumOptionsSelectValue()`, `errorId()`, `examplesId()`, `getClosestMatchingOption()`, `getFirstMatchingOption()`, `getInputProps()`, `helpId()`, `mergeValidationData()`, `optionId()`, `processSelectValue()`, `sanitizeDataForNewSchema()` and `titleId()`
- The following new utility functions were added: `ariaDescribedByIds()`, `createSchemaUtils()`, `descriptionId()`, `enumOptionsDeselectValue()`, `enumOptionsIndexForValue()`, `enumOptionsIsSelected()`, `enumOptionsSelectValue()`, `enumOptionsValueForIndex()`, `errorId()`, `examplesId()`, `getClosestMatchingOption()`, `getFirstMatchingOption()`, `getInputProps()`, `helpId()`, `mergeValidationData()`, `optionId()`, `sanitizeDataForNewSchema()` and `titleId()`
- `@rjsf/validator-ajv6`: The [ajv](https://github.com/ajv-validator/ajv)-v6-based validator refactored out of `@rjsf/[email protected]`, that implements the `ValidatorType` interface defined in `@rjsf/utils`.
- `@rjsf/validator-ajv8`: The [ajv](https://github.com/ajv-validator/ajv)-v8-based validator that is an upgrade of the `@rjsf/validator-ajv6`, that implements the `ValidatorType` interface defined in `@rjsf/utils`. See the ajv 6 to 8 [migration guide](https://ajv.js.org/v6-to-v8-migration.html) for more information.
- `@rjsf/mui`: Previously `@rjsf/material-ui/v5`, now provided as its own theme.
Expand All @@ -38,6 +38,13 @@ In many of the themes the `id`s for the `Title`, `Description` and `Examples` bl
In addition, some of the `id`s for various input values were updated to be consistent across themes or to fix small bugs.
For instance, the values for radio buttons in the `RadioWidget` and checkboxes in the `CheckboxesWidget` are of the form `xxx-${option.value}`, where `xxx` is the id of the field.

### `enumOptions[]` widgets BREAKING CHANGES

There are examples of schemas where `enumOptions[]` values are objects rather than primitive types.
In every theme, the `enumOptions[]` rendering widgets `CheckboxesWidget`, `RadioWidget` and `SelectWidget` previously used the `enumOptions[].value` to as the value used for the underlying `checkbox`, `radio` and `select.option` elements.
Now, these `CheckboxesWidget`, `RadioWidget` and `SelectWidget` components use the index of the `enumOptions[]` in the list as the value for the underlying elements.
If you need to build a custom widget for this kind of `enumOptions`, there are a set of `enumOptionsXXX` functions in `@rjsf/utils` to support your implementation.

### `@rjsf/core` BREAKING CHANGES

#### Types
Expand Down
62 changes: 44 additions & 18 deletions docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,65 @@ Return a consistent `id` for the field description element.
- string: The consistent id for the field description element from the given `id`

### enumOptionsDeselectValue\<S extends StrictRJSFSchema = RJSFSchema>()
Removes the `value` from the currently `selected` list of values.
Removes the enum option value at the `valueIndex` from the currently `selected` (list of) value(s).
If `selected` is a list, then that list is updated to remove the enum option value with the `valueIndex` in `allEnumOptions`.
If it is a single value, then if the enum option value with the `valueIndex` in `allEnumOptions` matches `selected`, undefined is returned, otherwise the `selected` value is returned.

#### Parameters
- value: EnumOptionsType\<S>["value"] - The value that should be selected
- selected: EnumOptionsType\<S>["value"][] - The current list of selected values
- valueIndex: string | number - The index of the value to be removed from the selected list or single value
- [selected]: EnumOptionsType\<S>["value"] | EnumOptionsType\<S>["value"][] | undefined - The current (list of) selected value(s)
- [allEnumOptions=[]]: EnumOptionsType\<S>[] - The list of all the known enumOptions

#### Returns
- EnumOptionsType\<S>["value"][]: The updated `selected` list with the `value` removed from it

### enumOptionsIndexForValue\<S extends StrictRJSFSchema = RJSFSchema>()
Returns the index(es) of the options in `allEnumOptions` whose value(s) match the ones in `value`.
All the `enumOptions` are filtered based on whether they are a "selected" `value` and the index of each selected one is then stored in an array.
If `multiple` is true, that array is returned, otherwise the first element in the array is returned.

#### Parameters
- value: EnumOptionsType\<S>["value"] | EnumOptionsType\<S>["value"][] - The single value or list of values for which indexes are desired
- [allEnumOptions=[]]: EnumOptionsType\<S>[] - The list of all the known enumOptions
- [multiple=false]: boolean - Optional flag, if true will return a list of index, otherwise a single one

#### Returns
- string | string[] | undefined: A single string index for the first `value` in `allEnumOptions`, if not `multiple`. Otherwise, the list of indexes for (each of) the value(s) in `value`.

### enumOptionsIsSelected\<S extends StrictRJSFSchema = RJSFSchema>()
Determines whether the given `value` is (one of) the `selected` value(s).

#### Parameters
- value: EnumOptionsType\<S>["value"] - The value being checked to see if it is selected
- selected: EnumOptionsType\<S>["value"] | EnumOptionsType\<S>["value"][] - The current selected value or list of values
- [allEnumOptions=[]]: EnumOptionsType\<S>[] - The list of all the known enumOptions

#### Returns
- boolean: true if the `value` is one of the `selected` ones, false otherwise

### enumOptionsSelectValue\<S extends StrictRJSFSchema = RJSFSchema>()
Add the `value` to the list of `selected` values in the proper order as defined by `allEnumOptions`.

#### Parameters
- value: EnumOptionsType\<S>["value"] - The value that should be selected
- valueIndex: string | number - The index of the value that should be selected
- selected: EnumOptionsType\<S>["value"][] - The current list of selected values
- allEnumOptions: EnumOptionsType\<S>[] - The list of all the known enumOptions
- [allEnumOptions=[]]: EnumOptionsType\<S>[] - The list of all the known enumOptions

#### Returns
- EnumOptionsType\<S>["value"][]: The updated list of selected enum values with `value` added to it in the proper location

### enumOptionsValueForIndex\<S extends StrictRJSFSchema = RJSFSchema>()
Returns the value(s) from `allEnumOptions` at the index(es) provided by `valueIndex`.
If `valueIndex` is not an array AND the index is not valid for `allEnumOptions`, `emptyValue` is returned.
If `valueIndex` is an array, AND it contains an invalid index, the returned array will have the resulting undefined values filtered out, leaving only valid values or in the worst case, an empty array.

#### Parameters
- valueIndex: string | number | Array<string | number> - The index(es) of the value(s) that should be returned
- [allEnumOptions=[]]: EnumOptionsType\<S>[] - The list of all the known enumOptions
- [emptyValue]: EnumOptionsType\<S>["value"] | undefined - The value to return when the non-array `valueIndex` does not refer to a real option
#### Returns
- EnumOptionsType\<S>["value"] | EnumOptionsType\<S>["value"][] | undefined: The single or list of values specified by the single or list of indexes if they are valid. Otherwise, `emptyValue` or an empty list.

### errorId<T = any>()
Return a consistent `id` for the field error element.

Expand Down Expand Up @@ -401,19 +440,6 @@ Parses the `dateString` into a `DateObject`, including the time information when
#### Throws
- Error when the date cannot be parsed from the string

### processSelectValue<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
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.
If the value is an empty string, then the `emptyValue` from the `options` is returned, falling back to undefined.

#### Parameters
- schema: S - The schema to used to determine the value's true type
- [value]: any - The value to convert
- [options]: UIOptionsType<T, S, F> | undefined - The UIOptionsType from which to potentially extract the `emptyValue`

#### Returns
- string | boolean | number | string[] | boolean[] | number[] | undefined: The `value` converted to the proper type

### rangeSpec\<S extends StrictRJSFSchema = RJSFSchema>()
Extracts the range spec information `{ step?: number, min?: number, max?: number }` that can be spread onto an HTML input from the range analog in the schema `{ multipleOf?: number, minimum?: number, maximum?: number }`.

Expand Down
29 changes: 22 additions & 7 deletions packages/antd/src/widgets/CheckboxesWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import Checkbox from "antd/lib/checkbox";
import {
ariaDescribedByIds,
enumOptionsIndexForValue,
enumOptionsValueForIndex,
optionId,
FormContextType,
WidgetProps,
Expand Down Expand Up @@ -33,15 +35,22 @@ export default function CheckboxesWidget<
}: WidgetProps<T, S, F>) {
const { readonlyAsDisabled = true } = formContext as GenericObjectType;

const { enumOptions, enumDisabled, inline } = options;
const { enumOptions, enumDisabled, inline, emptyValue } = options;

const handleChange = (nextValue: any) => onChange(nextValue);
const handleChange = (nextValue: any) =>
onChange(enumOptionsValueForIndex<S>(nextValue, enumOptions, emptyValue));

const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, target.value);
onBlur(
id,
enumOptionsValueForIndex<S>(target.value, enumOptions, emptyValue)
);

const handleFocus = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, target.value);
onFocus(
id,
enumOptionsValueForIndex<S>(target.value, enumOptions, emptyValue)
);

// Antd's typescript definitions do not contain the following props that are actually necessary and, if provided,
// they are used, so hacking them in via by spreading `extraProps` on the component to avoid typescript errors
Expand All @@ -51,18 +60,24 @@ export default function CheckboxesWidget<
onFocus: !readonly ? handleFocus : undefined,
};

const selectedIndexes = enumOptionsIndexForValue<S>(
value,
enumOptions,
true
) as string[];

return Array.isArray(enumOptions) && enumOptions.length > 0 ? (
<Checkbox.Group
disabled={disabled || (readonlyAsDisabled && readonly)}
name={id}
onChange={!readonly ? handleChange : undefined}
value={value}
value={selectedIndexes}
{...extraProps}
aria-describedby={ariaDescribedByIds<T>(id)}
>
{Array.isArray(enumOptions) &&
enumOptions.map((option, i) => (
<span key={option.value}>
<span key={i}>
<Checkbox
id={optionId<S>(id, option)}
name={id}
Expand All @@ -71,7 +86,7 @@ export default function CheckboxesWidget<
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(value) !== -1
}
value={option.value}
value={String(i)}
>
{option.label}
</Checkbox>
Expand Down
28 changes: 20 additions & 8 deletions packages/antd/src/widgets/RadioWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import Radio, { RadioChangeEvent } from "antd/lib/radio";
import {
ariaDescribedByIds,
enumOptionsIndexForValue,
enumOptionsValueForIndex,
optionId,
FormContextType,
GenericObjectType,
Expand Down Expand Up @@ -29,21 +31,31 @@ export default function RadioWidget<
onFocus,
options,
readonly,
schema,
value,
}: WidgetProps<T, S, F>) {
const { readonlyAsDisabled = true } = formContext as GenericObjectType;

const { enumOptions, enumDisabled } = options;
const { enumOptions, enumDisabled, emptyValue } = options;

const handleChange = ({ target: { value: nextValue } }: RadioChangeEvent) =>
onChange(schema.type === "boolean" ? nextValue !== "false" : nextValue);
onChange(enumOptionsValueForIndex<S>(nextValue, enumOptions, emptyValue));

const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, target.value);
onBlur(
id,
enumOptionsValueForIndex<S>(target.value, enumOptions, emptyValue)
);

const handleFocus = ({ target }: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, target.value);
onFocus(
id,
enumOptionsValueForIndex<S>(target.value, enumOptions, emptyValue)
);

const selectedIndexes = enumOptionsIndexForValue<S>(
value,
enumOptions
) as string;

return (
<Radio.Group
Expand All @@ -53,7 +65,7 @@ export default function RadioWidget<
onChange={!readonly ? handleChange : undefined}
onBlur={!readonly ? handleBlur : undefined}
onFocus={!readonly ? handleFocus : undefined}
value={`${value}`}
value={selectedIndexes}
aria-describedby={ariaDescribedByIds<T>(id)}
>
{Array.isArray(enumOptions) &&
Expand All @@ -65,8 +77,8 @@ export default function RadioWidget<
disabled={
Array.isArray(enumDisabled) && enumDisabled.indexOf(value) !== -1
}
key={option.value}
value={`${option.value}`}
key={i}
value={String(i)}
>
{option.label}
</Radio>
Expand Down
27 changes: 15 additions & 12 deletions packages/antd/src/widgets/SelectWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React from "react";
import Select, { DefaultOptionType } from "antd/lib/select";
import {
ariaDescribedByIds,
processSelectValue,
enumOptionsIndexForValue,
enumOptionsValueForIndex,
FormContextType,
GenericObjectType,
RJSFSchema,
Expand Down Expand Up @@ -36,21 +37,20 @@ export default function SelectWidget<
options,
placeholder,
readonly,
schema,
value,
}: WidgetProps<T, S, F>) {
const { readonlyAsDisabled = true } = formContext as GenericObjectType;

const { enumOptions, enumDisabled } = options;
const { enumOptions, enumDisabled, emptyValue } = options;

const handleChange = (nextValue: any) =>
onChange(processSelectValue<T, S, F>(schema, nextValue, options));
onChange(enumOptionsValueForIndex<S>(nextValue, enumOptions, emptyValue));

const handleBlur = () =>
onBlur(id, processSelectValue<T, S, F>(schema, value, options));
onBlur(id, enumOptionsValueForIndex<S>(value, enumOptions, emptyValue));

const handleFocus = () =>
onFocus(id, processSelectValue<T, S, F>(schema, value, options));
onFocus(id, enumOptionsValueForIndex<S>(value, enumOptions, emptyValue));

const filterOption = (input: string, option?: DefaultOptionType) => {
if (option && isString(option.label)) {
Expand All @@ -62,8 +62,11 @@ export default function SelectWidget<

const getPopupContainer = (node: any) => node.parentNode;

const stringify = (currentValue: any) =>
Array.isArray(currentValue) ? value.map(String) : String(value);
const selectedIndexes = enumOptionsIndexForValue<S>(
value,
enumOptions,
multiple
);

// Antd's typescript definitions do not contain the following props that are actually necessary and, if provided,
// they are used, so hacking them in via by spreading `extraProps` on the component to avoid typescript errors
Expand All @@ -82,20 +85,20 @@ export default function SelectWidget<
onFocus={!readonly ? handleFocus : undefined}
placeholder={placeholder}
style={SELECT_STYLE}
value={typeof value !== "undefined" ? stringify(value) : undefined}
value={selectedIndexes}
{...extraProps}
filterOption={filterOption}
aria-describedby={ariaDescribedByIds<T>(id)}
>
{Array.isArray(enumOptions) &&
enumOptions.map(({ value: optionValue, label: optionLabel }) => (
enumOptions.map(({ value: optionValue, label: optionLabel }, index) => (
<Select.Option
disabled={
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(optionValue) !== -1
}
key={String(optionValue)}
value={String(optionValue)}
key={String(index)}
value={String(index)}
>
{optionLabel}
</Select.Option>
Expand Down
Loading

0 comments on commit de5f153

Please sign in to comment.