Skip to content

Commit

Permalink
fix: Cast the value as an Array in CheckboxesWidget
Browse files Browse the repository at this point in the history
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
  • Loading branch information
heath-freenome committed Jan 15, 2023
1 parent 5db71e3 commit f6b0bb9
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 38 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,35 @@ 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)
Expand Down
13 changes: 7 additions & 6 deletions packages/bootstrap-4/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import {
WidgetProps,
} from "@rjsf/utils";

const selectValue = (value: any, selected: any, all: any) => {
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));
return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b)));
};

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

Expand All @@ -37,16 +37,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(selectValue(option.value, checkboxesValues, all));
} else {
onChange(deselectValue(option.value, value));
onChange(deselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -60,7 +61,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
7 changes: 4 additions & 3 deletions packages/core/src/components/widgets/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ 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;
Expand All @@ -50,9 +51,9 @@ function CheckboxesWidget<
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const all = enumOptions.map(({ value }) => value);
if (event.target.checked) {
onChange(selectValue(option.value, value, all));
onChange(selectValue(option.value, checkboxesValues, all));
} else {
onChange(deselectValue(option.value, value));
onChange(deselectValue(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
13 changes: 7 additions & 6 deletions packages/fluent-ui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ 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 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));
return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b)));
};

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

Expand All @@ -50,16 +50,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(selectValue(option.value, checkboxesValues, all));
} else {
onChange(deselectValue(option.value, value));
onChange(deselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -81,7 +82,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
13 changes: 7 additions & 6 deletions packages/material-ui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {
StrictRJSFSchema,
} from "@rjsf/utils";

const selectValue = (value: any, selected: any, all: any) => {
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));
return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b)));
};

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

Expand Down Expand Up @@ -47,16 +47,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(selectValue(option.value, checkboxesValues, all));
} else {
onChange(deselectValue(option.value, value));
onChange(deselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -75,7 +76,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
13 changes: 7 additions & 6 deletions packages/mui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {
StrictRJSFSchema,
} from "@rjsf/utils";

const selectValue = (value: any, selected: any, all: any) => {
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));
return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b)));
};

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

Expand Down Expand Up @@ -47,16 +47,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(selectValue(option.value, checkboxesValues, all));
} else {
onChange(deselectValue(option.value, value));
onChange(deselectValue(option.value, checkboxesValues));
}
};

Expand All @@ -75,7 +76,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
17 changes: 10 additions & 7 deletions packages/semantic-ui/src/CheckboxesWidget/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import { getSemanticProps } from "../util";

function selectValue<S extends StrictRJSFSchema = RJSFSchema>(
value: EnumOptionsType<S>["value"],
selected: 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));
return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b)));
}

function deselectValue<S extends StrictRJSFSchema = RJSFSchema>(
value: EnumOptionsType<S>["value"],
selected: any
selected: any[]
) {
return selected.filter((v: any) => v !== value);
}
Expand Down Expand Up @@ -61,6 +61,7 @@ export default function CheckboxesWidget<
options
);
const { enumOptions, enumDisabled, inline } = options;
const checkboxesValues = Array.isArray(value) ? value : [value];
const { title } = schema;
const semanticProps = getSemanticProps<T, S, F>({
options,
Expand All @@ -74,11 +75,13 @@ export default function CheckboxesWidget<
(option: EnumOptionsType) =>
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
// eslint-disable-next-line no-shadow
const all = enumOptions ? enumOptions.map(({ value }) => value) : [];
const all = enumOptions
? enumOptions.map(({ value }: EnumOptionsType<S>) => value)
: [];
if (checked) {
onChange(selectValue<S>(option.value, value, all));
onChange(selectValue<S>(option.value, checkboxesValues, all));
} else {
onChange(deselectValue<S>(option.value, value));
onChange(deselectValue<S>(option.value, checkboxesValues));
}
};

Expand All @@ -99,7 +102,7 @@ export default function CheckboxesWidget<
<Form.Group id={id} name={id} {...inlineOption}>
{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;
Expand Down

0 comments on commit f6b0bb9

Please sign in to comment.