Skip to content

Commit

Permalink
fix(useCheckboxGroup): make field initializable (#94) (#95)
Browse files Browse the repository at this point in the history
fixes #94
  • Loading branch information
MiroslavPetrik authored Jan 18, 2024
1 parent 5df7b99 commit 412f651
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 55 deletions.
14 changes: 10 additions & 4 deletions src/components/checkbox-group/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { StoryObj } from "@storybook/react";

import { CheckboxGroup } from "./CheckboxGroup";
import { CheckboxGroup, type CheckboxGroupProps } from "./CheckboxGroup";
import { CheckboxGroupField } from "./CheckboxGroupField.mock";
import { ZodArrayField, stringArrayField } from "../../fields";
import { UseCheckboxGroupProps } from "../../hooks";
import { StoryForm } from "../../scenarios/StoryForm";

const languagesOptions = [
Expand Down Expand Up @@ -32,8 +31,8 @@ export default meta;

const checkboxGroupStory = <Option, Field extends ZodArrayField>(
storyObj: {
args: Pick<UseCheckboxGroupProps<Option, Field>, "field"> &
Omit<Partial<UseCheckboxGroupProps<Option, Field>>, "field">;
args: Pick<CheckboxGroupProps<Option, Field>, "field"> &
Omit<Partial<CheckboxGroupProps<Option, Field>>, "field">;
} & Omit<StoryObj<typeof meta>, "args">,
) => ({
...storyObj,
Expand Down Expand Up @@ -62,6 +61,13 @@ export const Optional = checkboxGroupStory({
},
});

export const Initialized = checkboxGroupStory({
args: {
field: stringArrayField().optional(),
initialValue: ["ts", "hc"],
},
});

export const ComposedField = checkboxGroupStory({
args: {
field: stringArrayField(),
Expand Down
28 changes: 20 additions & 8 deletions src/components/checkbox-group/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { ZodArrayField } from "../../fields";
import { UseFieldOptions } from "form-atoms";

import { ZodArrayField, ZodFieldValue } from "../../fields";
import { UseCheckboxGroupProps, useCheckboxGroup } from "../../hooks";

export type CheckboxGroupProps<
Option,
Field extends ZodArrayField,
> = UseCheckboxGroupProps<Option, Field> &
Pick<UseFieldOptions<ZodFieldValue<Field>>, "initialValue">;

export const CheckboxGroup = <Option, Field extends ZodArrayField>({
field,
options,
getValue,
getLabel,
}: UseCheckboxGroupProps<Option, Field>) => {
const checkboxGroup = useCheckboxGroup({
field,
options,
getValue,
getLabel,
});
initialValue,
}: CheckboxGroupProps<Option, Field>) => {
const checkboxGroup = useCheckboxGroup(
{
field,
options,
getValue,
getLabel,
},
{ initialValue },
);

return (
<>
Expand Down
6 changes: 2 additions & 4 deletions src/components/checkbox-group/CheckboxGroupField.mock.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ReactNode } from "react";

import { CheckboxGroup } from "./CheckboxGroup";
import { ZodArrayField } from "../../fields";
import { UseCheckboxGroupProps } from "../../hooks";
import { CheckboxGroupFieldProps } from "../../hooks";
import { PicoFieldErrors } from "../../scenarios/PicoFieldErrors";
import { FieldLabel } from "../field-label";

Expand All @@ -12,7 +10,7 @@ export const CheckboxGroupField = <Option, Field extends ZodArrayField>({
getValue,
getLabel,
options,
}: { label: ReactNode } & UseCheckboxGroupProps<Option, Field>) => (
}: CheckboxGroupFieldProps<Option, Field>) => (
<>
<FieldLabel field={field} label={label} />
<p>
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/use-checkbox-group/useCheckboxGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ describe("useCheckboxGroup()", () => {
const options = ["electric", "gas", "manual"] as const;
const getValue = (opt: string) => opt;

test("initialize the field via options", () => {
// NOTE: test got stuck when the options are inlined
const fieldOptions = { initialValue: ["gas", "manual"] };

const field = stringArrayField();

const { result } = renderHook(() =>
useCheckboxGroup(
{ field, options, getValue, getLabel: getValue },
fieldOptions,
),
);

expect(result.current).toHaveLength(options.length);
expect(result.current[0]).toHaveProperty("checked", false);
expect(result.current[1]).toHaveProperty("checked", true);
expect(result.current[2]).toHaveProperty("checked", true);
});

describe("with required field", () => {
test("it has required checkboxes", () => {
const field = stringArrayField();
Expand Down
25 changes: 17 additions & 8 deletions src/hooks/use-checkbox-group/useCheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { UseFieldOptions } from "form-atoms";

import { useCheckboxGroupFieldProps } from "./useCheckboxGroupProps";
import { ZodArrayField } from "../../fields";
import { ZodArrayField, ZodFieldValue } from "../../fields";
import { FieldProps } from "../use-field-props";
import { type UseMultiSelectFieldProps } from "../use-multiselect-field-props";
import { type UseOptionsProps, useOptions } from "../use-options";

export type CheckboxGroupFieldProps<
Option,
Field extends ZodArrayField,
> = FieldProps<Field> & UseCheckboxGroupProps<Option, Field>;

export type UseCheckboxGroupProps<
Option,
Field extends ZodArrayField,
> = UseMultiSelectFieldProps<Option, Field> & UseOptionsProps<Option>;

export const useCheckboxGroup = <Option, Field extends ZodArrayField>({
field,
getValue,
getLabel,
options,
}: UseCheckboxGroupProps<Option, Field>) => {
const props = useCheckboxGroupFieldProps({ field, options, getValue });
export const useCheckboxGroup = <Option, Field extends ZodArrayField>(
{ field, getValue, getLabel, options }: UseCheckboxGroupProps<Option, Field>,
fieldOptions?: UseFieldOptions<ZodFieldValue<Field>>,
) => {
const props = useCheckboxGroupFieldProps(
{ field, options, getValue },
fieldOptions,
);

const { renderOptions } = useOptions({
field,
Expand Down
77 changes: 46 additions & 31 deletions src/hooks/use-checkbox-group/useCheckboxGroupProps.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,64 @@
import { UseFieldOptions } from "form-atoms";
import { useAtomValue } from "jotai";
import { ChangeEvent, useEffect, useState } from "react";
import { ChangeEvent, useMemo, useRef } from "react";

import {
UseMultiSelectFieldProps as UseCheckboxGroupFieldProps,
type UseMultiSelectFieldProps as UseCheckboxGroupFieldProps,
useFieldProps,
} from "../";
import { ZodArrayField, ZodFieldValue } from "../../fields";

export const useCheckboxGroupFieldProps = <
Option,
Field extends ZodArrayField,
>({
field,
options,
getValue,
}: UseCheckboxGroupFieldProps<Option, Field>) => {
import type { ZodArrayField, ZodFieldValue } from "../../fields";

export const useCheckboxGroupFieldProps = <Option, Field extends ZodArrayField>(
{ field, options, getValue }: UseCheckboxGroupFieldProps<Option, Field>,
fieldOptions?: UseFieldOptions<ZodFieldValue<Field>>,
) => {
const atom = useAtomValue(field);
const fieldValue = useAtomValue(atom.value);
const [value, setValue] = useState<number[]>(() =>
fieldValue.map((activeOption) => options.indexOf(activeOption)),
const optionValues = useMemo(
() => options.map(getValue),
[getValue, options],
);

const prevValue = useRef(fieldValue);

const activeIndexes = useRef<number[]>(
fieldValue.map((activeOption) => optionValues.indexOf(activeOption)),
);

if (prevValue.current != fieldValue) {
/**
* The field was set from outside via initialValue, reset action, or set manually.
* Recompute the indexes.
**/
activeIndexes.current = fieldValue.map((activeOption) =>
optionValues.indexOf(activeOption),
);
}

const getEventValue = (event: ChangeEvent<HTMLInputElement>) => {
const index = parseInt(event.currentTarget.value);
const values = event.currentTarget.checked
? [...value, index]
: value.filter((val) => val != index);
const nextIndexes = event.currentTarget.checked
? [...activeIndexes.current, index]
: activeIndexes.current.filter((val) => val != index);

setValue(values);
activeIndexes.current = nextIndexes;

const activeOptions = values
.map((idx) => options[idx])
.filter(Boolean) as Option[];
const nextValues = nextIndexes.map((index) => optionValues[index]);

return activeOptions.map(getValue) as ZodFieldValue<Field>;
};
/**
* When user change event happened, we set the value.
* On the next render when the fieldValue is updated, we can skip calculating the activeIndexes.
*/
prevValue.current = nextValues;

useEffect(() => {
if (fieldValue.length === 0 && value.length !== 0) {
// reset local state, when form was reset
setValue([]);
}
}, [fieldValue]);
return nextValues as ZodFieldValue<Field>;
};

const props = useFieldProps<Field, HTMLInputElement>(field, getEventValue);
const props = useFieldProps<Field, HTMLInputElement>(
field,
getEventValue,
fieldOptions,
);

return { ...props, value };
return { ...props, value: activeIndexes.current };
};

0 comments on commit 412f651

Please sign in to comment.