Skip to content

Commit

Permalink
fix: Cast the value as an Array in CheckboxesWidget (rjsf-team#3379)
Browse files Browse the repository at this point in the history
* fix: Cast the value as an Array in CheckboxesWidget
Fixes rjsf-team#2141 by reimplementing rjsf-team#2142

When the value passed to the `CheckboxesWidget` was a single value rather than an array, things would break in the control
- Updated the `CheckboxesWidget` in all themes but `antd` (which uses simpler logic) to fix ensure the value used in the helper functions is an array
- Added a test in `ArrayField_test.ts` that verifies the fix
- Updated the snapshot in `bootstrap-4` which showed an issue when the value string contained an element of the enumeration value rather than the whole value
- Refactored the common `selectValue` and `deselectValue` functions used in `CheckboxesWidget` into `@rjsf/utils`
- Updated the `CHANGELOG.md` accordingly

* - Responded to reviewer feedback, including refactoring the `enumOptionsDeselectValue()` and `enumOptionsSelectValue()` from all the `CheckboxesWidget` implementations into `@rjsf/utils`
  • Loading branch information
heath-freenome authored and shijistar committed Jun 8, 2023
1 parent 05e6dfa commit 3d7ca80
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 118 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,39 @@ should change the heading of the (upcoming) version to include a major version b

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @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)
- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141)

## @rjsf/utils
- Updated the `FieldTemplateProps`, `WrapIfAdditionalTemplateProps` and `UIOptionsBaseType` types to add `style?: StyleHTMLAttributes<any>`, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200)
- Updated the `FieldTemplateProps`, `WrapIfAdditionalTemplateProps` and `UIOptionsBaseType` types to add `style?: StyleHTMLAttributes<any>`, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200)
- Added `enumOptionsDeselectValue()` and `enumOptionsSelectValue()` as a loose refactor of the duplicated functions in the various `CheckboxesWidget` implementations

## @rjsf/validator-ajv8
- Remove alias for ajv -> ajv8 in package.json. This fixes [#3215](https://github.com/rjsf-team/react-jsonschema-form/issues/3215).
Expand All @@ -57,6 +65,7 @@ should change the heading of the (upcoming) version to include a major version b
- 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
- Updated the `utility-functions` documentation to add the new `enumOptionsDeselectValue()` and `enumOptionsSelectValue()` functions

# 5.0.0-beta-16

Expand Down
22 changes: 21 additions & 1 deletion docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,28 @@ Implements a deep equals using the `lodash.isEqualWith` function, that provides
#### Returns
- boolean: True if the `a` and `b` are deeply equal, false otherwise

### findSchemaDefinition\<S extends StrictRJSFSchema = RJSFSchema>()
### enumOptionsDeselectValue\<S extends StrictRJSFSchema = RJSFSchema>()
Removes the `value` from the currently `selected` list of values.

#### Parameters
- value: EnumOptionsType<S>["value"] - The value that should be selected
- selected: EnumOptionsType<S>["value"][] - The current list of selected values

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

### 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
- selected: EnumOptionsType<S>["value"][] - The current list of selected values
- 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

### findSchemaDefinition\<S extends StrictRJSFSchema = RJSFSchema>()
Given the name of a `$ref` from within a schema, using the `rootSchema`, look up and return the sub-schema using the path provided by that reference.
If `#` is not the first character of the reference, or the path does not exist in the schema, then throw an Error.
Otherwise return the sub-schema. Also deals with nested `$ref`s in the sub-schema.
Expand Down
26 changes: 8 additions & 18 deletions packages/bootstrap-4/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import React from "react";
import Form from "react-bootstrap/Form";
import {
enumOptionsDeselectValue,
enumOptionsSelectValue,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WidgetProps,
} from "@rjsf/utils";

const selectValue = (value: any, selected: any, all: any) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));

// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b));
};

const deselectValue = (value: any, selected: any) => {
return selected.filter((v: any) => v !== value);
};

export default function CheckboxesWidget<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
Expand All @@ -37,16 +26,17 @@ export default function CheckboxesWidget<
onFocus,
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled, inline } = options;
const checkboxesValues = Array.isArray(value) ? value : [value];

const _onChange =
(option: any) =>
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
const all = (enumOptions as any).map(({ value }: any) => value);

if (checked) {
onChange(selectValue(option.value, value, all));
onChange(
enumOptionsSelectValue(option.value, checkboxesValues, enumOptions)
);
} else {
onChange(deselectValue(option.value, value));
onChange(enumOptionsDeselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -60,7 +50,7 @@ export default function CheckboxesWidget<
<Form.Group>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index: number) => {
const checked = value.indexOf(option.value) !== -1;
const checked = checkboxesValues.includes(option.value);
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`CheckboxesWidget inline 1`] = `
>
<input
autoFocus={true}
checked={true}
checked={false}
className="custom-control-input"
disabled={true}
id="_id-a"
Expand Down Expand Up @@ -40,7 +40,7 @@ exports[`CheckboxesWidget simple 1`] = `
>
<input
autoFocus={true}
checked={true}
checked={false}
className="custom-control-input"
disabled={true}
id="_id-a"
Expand Down
3 changes: 2 additions & 1 deletion packages/chakra-ui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function CheckboxesWidget<
} = props;
const { enumOptions, enumDisabled } = options;
const chakraProps = getChakra({ uiSchema });
const checkboxesValues = Array.isArray(value) ? value : [value];

const _onBlur = ({
target: { value },
Expand Down Expand Up @@ -66,7 +67,7 @@ export default function CheckboxesWidget<
<Stack direction={row ? "row" : "column"}>
{Array.isArray(enumOptions) &&
enumOptions.map((option) => {
const checked = value.indexOf(option.value) !== -1;
const checked = checkboxesValues.includes(option.value);
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
Expand Down
30 changes: 14 additions & 16 deletions packages/core/src/components/widgets/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import React, { ChangeEvent } from "react";
import {
enumOptionsDeselectValue,
enumOptionsSelectValue,
FormContextType,
WidgetProps,
RJSFSchema,
StrictRJSFSchema,
} from "@rjsf/utils";

function selectValue(value: any, selected: any[], all: any[]) {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));
// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b)));
}

function deselectValue(value: any, selected: any[]) {
return selected.filter((v) => v !== value);
}

/** The `CheckboxesWidget` is a widget for rendering checkbox groups.
* It is typically used to represent an array of enums.
*
Expand All @@ -36,23 +26,31 @@ function CheckboxesWidget<
readonly,
onChange,
}: WidgetProps<T, S, F>) {
const checkboxesValues = Array.isArray(value) ? value : [value];
return (
<div className="checkboxes" id={id}>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index) => {
const checked = value.indexOf(option.value) !== -1;
const checked = checkboxesValues.includes(option.value);
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) != -1;
const disabledCls =
disabled || itemDisabled || readonly ? "disabled" : "";

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const all = enumOptions.map(({ value }) => value);
if (event.target.checked) {
onChange(selectValue(option.value, value, all));
onChange(
enumOptionsSelectValue(
option.value,
checkboxesValues,
enumOptions
)
);
} else {
onChange(deselectValue(option.value, value));
onChange(
enumOptionsDeselectValue(option.value, checkboxesValues)
);
}
};

Expand Down
33 changes: 32 additions & 1 deletion packages/core/test/ArrayField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,38 @@ describe("ArrayField", () => {
);
});

it("should fill field with data", () => {
it("should fill properly field with data that is not an array and handle change event", () => {
const { node, onChange } = createFormComponent({
schema,
uiSchema,
formData: "foo",
});

let labels = [].map.call(
node.querySelectorAll("[type=checkbox]"),
(node) => node.checked
);
expect(labels).eql([true, false, false]);

Simulate.change(node.querySelectorAll("[type=checkbox]")[2], {
target: { checked: true },
});

sinon.assert.calledWithMatch(
onChange.lastCall,
{
formData: ["foo", "fuzz"],
},
"root"
);
labels = [].map.call(
node.querySelectorAll("[type=checkbox]"),
(node) => node.checked
);
expect(labels).eql([true, false, true]);
});

it("should fill field with array of data", () => {
const { node } = createFormComponent({
schema,
uiSchema,
Expand Down
26 changes: 8 additions & 18 deletions packages/fluent-ui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import { Checkbox, Label } from "@fluentui/react";
import {
enumOptionsDeselectValue,
enumOptionsSelectValue,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
Expand All @@ -17,19 +19,6 @@ const styles_red = {
fontFamily: `"Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;`,
};

const selectValue = (value: any, selected: any, all: any) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));

// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b));
};

const deselectValue = (value: any, selected: any) => {
return selected.filter((v: any) => v !== value);
};

export default function CheckboxesWidget<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
Expand All @@ -50,16 +39,17 @@ export default function CheckboxesWidget<
rawErrors = [],
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled } = options;
const checkboxesValues = Array.isArray(value) ? value : [value];

const _onChange =
(option: any) =>
(_ev?: React.FormEvent<HTMLElement>, checked?: boolean) => {
const all = (enumOptions as any).map(({ value }: any) => value);

if (checked) {
onChange(selectValue(option.value, value, all));
onChange(
enumOptionsSelectValue(option.value, checkboxesValues, enumOptions)
);
} else {
onChange(deselectValue(option.value, value));
onChange(enumOptionsDeselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -81,7 +71,7 @@ export default function CheckboxesWidget<
</Label>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index: number) => {
const checked = value.indexOf(option.value) !== -1;
const checked = checkboxesValues.includes(option.value);
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
Expand Down
26 changes: 8 additions & 18 deletions packages/material-ui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,14 @@ import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormGroup from "@material-ui/core/FormGroup";
import FormLabel from "@material-ui/core/FormLabel";
import {
enumOptionsDeselectValue,
enumOptionsSelectValue,
FormContextType,
WidgetProps,
RJSFSchema,
StrictRJSFSchema,
} from "@rjsf/utils";

const selectValue = (value: any, selected: any, all: any) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));

// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b));
};

const deselectValue = (value: any, selected: any) => {
return selected.filter((v: any) => v !== value);
};

/** The `CheckboxesWidget` is a widget for rendering checkbox groups.
* It is typically used to represent an array of enums.
*
Expand All @@ -47,16 +36,17 @@ export default function CheckboxesWidget<
onFocus,
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled, inline } = options;
const checkboxesValues = Array.isArray(value) ? value : [value];

const _onChange =
(option: any) =>
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
const all = (enumOptions as any).map(({ value }: any) => value);

if (checked) {
onChange(selectValue(option.value, value, all));
onChange(
enumOptionsSelectValue(option.value, checkboxesValues, enumOptions)
);
} else {
onChange(deselectValue(option.value, value));
onChange(enumOptionsDeselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -75,7 +65,7 @@ export default function CheckboxesWidget<
<FormGroup id={id} row={!!inline}>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index: number) => {
const checked = value.indexOf(option.value) !== -1;
const checked = checkboxesValues.includes(option.value);
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
Expand Down
Loading

0 comments on commit 3d7ca80

Please sign in to comment.